談談函數式響應式編程(Functional Reactive Programming)

目錄

What is Functional Programming

第一部分概念較多, 如果沒有耐心看下去的話, 可直接看重點標注的定義

What is side effects

Let's take a look at the first pair with this example

public int square(int x) {
    return x * x;
}

Here, the input you're used to thinking about is int x, and the output you're used to is also an int

public void processNext() {
    Message message = InboxQueue.popMessage();

    if (message != null) {
        process(message);
    }
}

The second piece of code has hidden inputs and outputs. It requires things, and causes things, but you could never guess what just by looking at the API

These hidden inputs and outputs have an official name: "side-effects"

Are Side Effects Bad?

we have to trust that the the hidden expectations of the original programmer were correct, and will remain correct as time marches on

Can we test this code? Not in isolation. Unlike a circuit board, we can't just plug into its inputs and check its outputs

What is a Pure Function

A function is called 'pure' if all its inputs are declared as inputs - none of them are hidden - and likewise all its outputs are declared as outputs

What is Functional Programming

Functional programming is about writing pure functions, about removing hidden inputs and outputs as far as we can, so that as much of our code as possible just describes a relationship between inputs and outputs

What is a Functional Programming Language

A functional programming language is one that supports and encourages programming without side-effects

Features of functional languages

  • First-Class Functions - Functional programming requires that functions are first-class, which means that they are treated like any other values and can be passed as arguments to other functions or be returned as a result of a function

  • Pure Function - 函數的結果只受函數參數影響, 且不影響外部變量 / 函數內部不使用能被外部函數影響的變量

  • Immutable Data - Purely functional programs typically operate on immutable data. Instead of altering existing values, altered copies are created and the original is preserved

  • Lazy Evaluation - Since pure computations are referentially transparent they can be performed at any time and still yield the same result. This makes it possible to defer the computation of values until they are needed, that is, to compute them lazily

  • Recursion

Recursion is heavily used in functional programming as it is the canonical and often the only way to iterate. Functional language implementations will often include tail call optimisation to ensure that heavy recursion does not consume excessive memory

Lazy evaluation avoids unnecessary computations and allows, for example, infinite data structures to be defined and used

In functional programming, programs are executed by evaluating expressions

In contrast with imperative programming where programs are composed of statements which change global state when executed. Functional programming typically avoids using mutable state

Benefits of functional programming

Functional programming is known to provide better support for structured programming than imperative programming. To make a program structured it is necessary to develop abstractions and split it into components which interface each other with those abstractions. Functional languages aid this by making it easy to create clean and simple abstractions. It is easy, for instance, to abstract out a recurring piece of code by creating a higher-order function, which will make the resulting code more declarative and comprehensible

Functional programs are often shorter and easier to understand than their imperative counterparts

Since various studies have shown that the average programmer's productivity in terms of lines of code is more or less the same for any programming language, this translates also to higher productivity

Functional Programming in iOS

Masonry

iOS開發中鏈式函數編程的典例:

make.centerY.equalTo(self.view).offset(100);

如何實現類似masonry的鏈式函數編程呢? (引自深入淺出-iOS函數式編程的實現 && 響應式編程概念)

有一個Person類, 它的兩個方法如下

- (void)run{
    NSLog(@"run");
}

- (void)study {
    NSLog(@"study")
}

要實現這樣的效果

person.run().study().run();

我們可以這樣做

- (Person * (^)())run {  
    Person * (^block)() = ^() {            
        NSLog(@"run");           
        return self;        
    };       
    return block;
}

- (Person * (^)())study {
    Person * (^block)() = ^()  {            
        NSLog(@"study"); 
        return self;        
    };
    return block;
}

事實上masonry也是這么實現的

- (MASConstraint * (^)(id))equalTo {
    return ^id(id attribute) {
        return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
    };
}

PromiseKit

我們來看看PromiseKit實現UI Animation的例子(更多例子, 請參考PromiseKit官網)

[UIView promiseWithDuration:0.3 animations:^{
    view.frame = CGRectOffset(view.frame, 0, -100);
}].then(^{
    return [self doWork];
}).catch(^(NSError *error){
    [[UIAlertView …] show];
}).finally(^{
    view.frame = CGRectOffset(view.frame, 0, +100);
})

Swift

盡管我不認為Swift是一門函數式編程語言(對于語言的討論可以參考Which Programming Languages Are Functional?), 但是相比于Objective-C來說Swift最大優點就是

Swift為iOS編程世界引入了一個新的范式: 函數式范式

我們來看這樣一個例子(代碼的含義比較簡單, 就不解釋了)

var evens = [Int]()
for i in 1...10 {
  if i % 2 == 0 {
    evens.append(i)
  }
}
println(evens)

使用Functional Filtering可以寫成這樣

func isEven(number: Int) -> Bool {
  return number % 2 == 0
}
evens = Array(1...10).filter(isEven)
println(evens)

運用Swift的closures以上代碼還可以簡化成

evens = Array(1...10).filter { (number) in number % 2 == 0 }
println(evens)

更甚

evens = Array(1...10).filter { $0 % 2 == 0 }
println(evens)

Functional Programming in Android

Glide

Glide作為新生代Android圖片庫的代表, 和Fresco都是值得推薦的

Glide, Fresco與Picasso的比較可以參考[ Android ] Fresco 與 Picasso 、Glide 的比較Picasso&Glide&Fresco比較

Glide的使用非常簡單和函數式

// For a simple view:
@Override public void onCreate(Bundle savedInstanceState) {
  ...
  ImageView imageView = (ImageView) findViewById(R.id.my_image_view);

  Glide.with(this).load("http://goo.gl/gEgYUd").into(imageView);
}

// For a simple image list:
@Override public View getView(int position, View recycled, ViewGroup container) {
  final ImageView myImageView;
  if (recycled == null) {
    myImageView = (ImageView) inflater.inflate(R.layout.my_image_view, container, false);
  } else {
    myImageView = (ImageView) recycled;
  }

  String url = myUrls.get(position);

  Glide
    .with(myFragment)
    .load(url)
    .centerCrop()
    .placeholder(R.drawable.loading_spinner)
    .crossFade()
    .into(myImageView);

  return myImageView;
}

What is Reactive Programming

以下是Wiki Reactive Programming的定義

reactive programming is a programming paradigm oriented around data flows and the propagation of change

其實響應式這個概念很早就有了, 例如

  • 當你操作鍵盤時, 電腦對按鍵進行中斷響應

  • 當你瀏覽網站時, 頁面對點擊頁面按鈕的響應

那么響應式編程的目的是什么呢? 我們來看這樣一個經典的例子(引自Wiki Reactive Programming)

For example, in an imperative programming setting, a b c

a = b + c would mean that a is being assigned the result of b+c in the instant the expression is evaluated

and later, the values of b and c can be changed with no effect on the value of a

However, in reactive programming, the value of a would be automatically updated based on the new values

而響應式編程也是很早就有的了, 例如

  • iOS開發中的KVC

  • Android開發中的Broadcast

從中可以看出一點, 所謂的響應式編程很多是基于event bus或observer的, 那么有沒有一種更友好, 更強大, 更智能的響應式編程呢?

答案就是本文的主題: Functional Reactive Programming

What is Functional Reactive Programming

按照"國際慣例", 先看下Wiki FRP的定義

Functional reactive programming (FRP) is a programming paradigm for reactive programming (asynchronous dataflow programming) using the building blocks of functional programming (e.g. map, reduce, filter)

簡單來說

Functional Reactive Programming = Functional Programming + Reactive Programming

由此, 函數響應式編程集合了兩家之所長

那為什么是Functional Programming和Reactive Programming在"一起"了呢? 我認為因為他們兩者都最"合適"

  • 函數式編程的這種輸入輸出特性, 本身就帶著些響應式的味道

  • 響應式以往的實現都沒有函數式這樣的: 簡潔, 流暢(想想鏈式的絲滑)和高度統一(想想filter, map, 再對比下各種廣播觀察者event bus云云)

Functional Reactive Programming in iOS

ReactiveCocoa

在iOS開發中, 現在一說到函數響應式或MVVM首先想到的應該就是ReactiveCocoa了

我們來看一個ReactiveCocoa的例子(引自ReactiveCocoa Tutorial – The Definitive Introduction: Part 1/2)

[[[self.usernameTextField.rac_textSignal
    map:^id(NSString *text) {
        return @(text.length);
    }]
    filter:^BOOL(NSNumber *length) {
        return [length integerValue] > 3;
    }]
    subscribeNext:^(id x) {
        NSLog(@"%@", x);
    }];
FilterAndMapPipeline.png

RxSwift

iOS開發中, 如果你已經使用Swift語言開發, 除了ReactiveCocoa還可以考慮RxJava

它的最大優勢就是屬于ReactiveX旗下的Swift版本, 精通了RxSwift就可以無縫切換至RxJS, RxJava, RxAndroid,Rx.Net, RxRuby...

我們來看一個用RxSwift實現UITableView的例子(引自 【RxSwift系列】用RxSwift實現一個UITableView(一))

ViewModel的方法看起來應該是這樣的

PS: 在真實應用中, 數據應該是通過網絡請求解析JSON數據而獲得來的

import Foundation
import RxSwift
import RxDataSources

class ViewModel: NSObject {

    func getUsers() -> Observable<[SectionModel<String, User>]> {
    return Observable.create { (observer) -> Disposable in
        let users = [User(followersCount: 19_901_990, followingCount: 1990, screenName: "Marco Sun"),
            User(followersCount: 19_890_000, followingCount: 1989, screenName: "Taylor Swift"),
            User(followersCount: 250_000, followingCount: 25, screenName: "Rihanna"),
            User(followersCount: 13_000_000_000, followingCount: 13, screenName: "Jolin Tsai"),
            User(followersCount: 25_000_000, followingCount: 25, screenName: "Adele")]
        let section = [SectionModel(model: "", items: users)]
        observer.onNext(section)
        observer.onCompleted()
        return AnonymousDisposable{}
    }
}

viewDidLoad()方法是這樣的

override func viewDidLoad() {

    super.viewDidLoad()
    view.addSubview(tableView)
    tableView.registerClass(TableViewCell.self, forCellReuseIdentifier: reuseIdentifier)

    viewModel.getUsers()
        .bindTo(tableView.rx_itemsWithDataSource(dataSource))
        .addDisposableTo(disposeBag)
}

Functional Reactive Programming in Android

RxJava

The example below shows what you may already be familiar with:

It calls to the web service, uses a callbacks interface to pass the successful result to the next web service call, define another success callback, and then moves on to the next web service request

As you can see, this results in two nested callbacks

Old Style

// The "Nested Callbacks" Way
    public void fetchUserDetails() {
        // first, request the users...
        mService.requestUsers(new Callback<GithubUsersResponse>() {
            @Override
            public void success(final GithubUsersResponse githubUsersResponse,
                                final Response response) {
                Timber.i(TAG, "Request Users request completed");
                final List<GithubUserDetail> githubUserDetails = new ArrayList<GithubUserDetail>();
                // next, loop over each item in the response
                for (GithubUserDetail githubUserDetail : githubUsersResponse) {
                    // request a detail object for that user
                    mService.requestUserDetails(githubUserDetail.mLogin,
                                                new Callback<GithubUserDetail>() {
                        @Override
                        public void success(GithubUserDetail githubUserDetail,
                                            Response response) {
                            Log.i("User Detail request completed for user : " + githubUserDetail.mLogin);
                            githubUserDetails.add(githubUserDetail);
                            if (githubUserDetails.size() == githubUsersResponse.mGithubUsers.size()) {
                                // we've downloaded'em all - notify all who are interested!
                                mBus.post(new UserDetailsLoadedCompleteEvent(githubUserDetails));
                            }
                        }

                        @Override
                        public void failure(RetrofitError error) {
                            Log.e(TAG, "Request User Detail Failed!!!!", error);
                        }
                    });
                }
            }

            @Override
            public void failure(RetrofitError error) {
                Log.e(TAG, "Request User Failed!!!!", error);
            }
        });
    }

Now, let’s look at the same functionality written with RxJava

public void rxFetchUserDetails() {
        //request the users
        mService.rxRequestUsers().concatMap(Observable::from)
        .concatMap((GithubUser githubUser) ->
                        //request the details for each user
                        mService.rxRequestUserDetails(githubUser.mLogin)
        )
        //accumulate them as a list
        .toList()
        //define which threads information will be passed on
        .subscribeOn(Schedulers.newThread())
        .observeOn(AndroidSchedulers.mainThread())
        //post them on an eventbus
        .subscribe(githubUserDetails -> {
            EventBus.getDefault().post(new UserDetailsLoadedCompleteEvent(githubUserDetails));
        });
    }

RetroFit

現在Android很火的RetroFit結合RxJava就可以寫出"動人"的函數響應式實現來

NetworkUtil.getTestObjectRxApi().getTestObjects()
        .doOnNext(new Action1<Map<String, List<TestObjectBean>>>() {
            @Override
            public void call(Map<String, List<TestObjectBean>> stringListMap) {
                Logger.e(Thread.currentThread().getName());
                TestObjectBean testObjectBean = stringListMap.get("results").get(0);
                Logger.d("objectId = " + testObjectBean.getOb());
                testObjectBean.setOb("new id");
            }
        })
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(new Observer<Map<String, List<TestObjectBean>>>() {
            @Override
            public void onCompleted() {
                Logger.e(Thread.currentThread().getName());
                Logger.d("completed");
            }

            @Override
            public void onError(Throwable e) {
                Logger.e(e.getMessage());
            }

            @Override
            public void onNext(Map<String, List<TestObjectBean>> stringListMap) {
                Logger.e(Thread.currentThread().getName());
                for (TestObjectBean testObjectBean : stringListMap.get("results")) {
                    Logger.d(testObjectBean);
                }
            }
        });

References

更多文章, 請支持我的個人博客

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,321評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,559評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,442評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,835評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,581評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,922評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,931評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,096評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,639評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,374評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,591評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,104評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,789評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,196評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,524評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,322評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,554評論 2 379

推薦閱讀更多精彩內容