函數(shù)響應(yīng)式編程(FRP)從入門到"放棄"——基礎(chǔ)概念篇

前言

研究ReactiveCocoa一段時間了,是時候總結(jié)一下學(xué)到的一些知識了僵驰。

一.函數(shù)響應(yīng)式編程

說道函數(shù)響應(yīng)式編程喷斋,就不得不提到函數(shù)式編程唁毒,它們倆到底有什么關(guān)系呢?今天我們就詳細(xì)的解析一下他們的關(guān)系星爪。

現(xiàn)在有下面4個概念浆西,需要我們理清一下它們之間的關(guān)系:
面向?qū)ο缶幊?Object Oriented Programming
響應(yīng)式編程 Reactive Programming
函數(shù)式編程 Functional Programming
函數(shù)響應(yīng)式編程 Functional Reactive Programming

我們先來說說什么是函數(shù)式編程Functional Programming,我們先來看看wikipedia上的相關(guān)定義:

Functional Programming is a programming paradigm

  1. treats computation as the evaluation of mathematical functions.
  2. avoids changing-state and mutable data

總結(jié)一下函數(shù)式編程具有以下幾個特點:

  1. 函數(shù)是"第一等公民"
  2. 閉包和高階函數(shù)
  3. 不改變狀態(tài)(由此延伸出”引用透明”的概念)
  4. 遞歸
  5. 只用"表達(dá)式"顽腾,不用"語句"近零,沒有副作用

接下來我們依次說明一下這些特點。

一. 函數(shù)是"第一等公民"

所謂"第一等公民"(first class)抄肖,指的是函數(shù)與其他數(shù)據(jù)類型一樣久信,處于平等地位,可以賦值給其他變量憎瘸,也可以作為參數(shù)入篮,傳入另一個函數(shù),或者作為別的函數(shù)的返回值幌甘。

一等函數(shù)的理念可以追溯到 Church 的 lambda 演算 (Church 1941; Barendregt 1984)潮售。此后,包括 Haskell锅风,OCaml酥诽,Standard ML,Scala 和 F# 在內(nèi)的大量 (函數(shù)式) 編程語言都不同程度地借鑒了這個概念皱埠。

Ps:世界上最純粹的函數(shù)式編程語言非Haskell莫屬肮帐。

二.閉包和高階函數(shù)

閉包是起函數(shù)的作用并可以像對象一樣操作的對象。與此類似边器,函數(shù)式編程語言支持高階函數(shù)训枢。高階函數(shù)可以用另一個函數(shù)(間接地,用一個表達(dá)式) 作為其輸入?yún)?shù)忘巧,在大多數(shù)情況下恒界,它甚至返回一個函數(shù)作為其輸出參數(shù)。這兩種結(jié)構(gòu)結(jié)合在一起使得可以用優(yōu)雅的方式進(jìn)行模塊化編程砚嘴,這是使用函數(shù)式編程的最大好處十酣。

三. 不改變狀態(tài)(由此延伸出”引用透明”的概念)

不改變狀態(tài):
函數(shù)式編程只是返回新的值,不修改系統(tǒng)變量际长。因此耸采,不修改變量,也是它的一個重要特點工育。在其他類型的語言中虾宇,變量往往用來保存"狀態(tài)"(state)。不修改變量翅娶,意味著狀態(tài)不能保存在變量中文留。函數(shù)式編程使用參數(shù)保存狀態(tài)好唯,最好的例子就是遞歸竭沫。

避免使用程序狀態(tài)和可變對象燥翅,是降低程序復(fù)雜度的有效方式之一,而這也正是函數(shù)式編程的精髓蜕提。函數(shù)式編程強(qiáng)調(diào)執(zhí)行的結(jié)果森书,而非執(zhí)行的過程。我們先構(gòu)建一系列簡單卻具有一定功能的小函數(shù)谎势,然后再將這些函數(shù)進(jìn)行組裝以實現(xiàn)完整的邏輯和復(fù)雜的運算凛膏,這是函數(shù)式編程的基本思想。

引用透明:
如果提供同樣的輸入脏榆,那么函數(shù)總是返回同樣的結(jié)果猖毫。就是說,表達(dá)式的值不依賴于可以改變值的全局狀態(tài)须喂。這使您可以從形式上推斷程序行為吁断,因為表達(dá)式的意義只取決于其子表達(dá)式而不是計算順序或者其他表達(dá)式的副作用。

這里有出現(xiàn)了一個問題:

面試題:** 純函數(shù)式的閉包是否滿足函數(shù)式編程里面不改變函數(shù)狀態(tài)的特性坞生?**

根據(jù)純函數(shù)的定義

在計算機(jī)編程中仔役,假如滿足下面這兩個句子的約束,一個函數(shù)可能被描述為一個純函數(shù):

  1. 給出同樣的參數(shù)值是己,該函數(shù)總是求出同樣的結(jié)果又兵。該函數(shù)結(jié)果值不依賴任何隱藏信息或程序執(zhí)行處理可能改變的狀態(tài)或在程序的兩個不同的執(zhí)行,也不能依賴來自I/O裝置的任何外部的輸入(通常是這樣的--看下面的描述)卒废。
  1. 結(jié)果的求值不會促使任何可語義上可觀察的副作用或輸出沛厨,例如易變對象的變化或輸出到I/O裝置。

函數(shù)的返回值是不需要依賴所有(或任何)參數(shù)值摔认,必須不依賴參數(shù)值以外的東西逆皮。函數(shù)可能返回多重結(jié)果值,并且對于被認(rèn)為是純函數(shù)的函數(shù)级野,這些條件必須應(yīng)用到所有返回值页屠。假如一個參數(shù)通過引用調(diào)用,任何內(nèi)部參數(shù)變化將改變函數(shù)外部的輸入?yún)?shù)值蓖柔,它將使函數(shù)變?yōu)榉羌兒瘮?shù)辰企。

回到我們討論的這個問題上來:

閉包雖然可以把閉包外部的變量捕獲到閉包內(nèi)部,但是閉包還是滿足不改變狀態(tài)的特性的况鸣。假設(shè)f(x)的返回值是g(x)牢贸,而g(x)是會依靠f(x)的參數(shù)返回的,g(x)相當(dāng)于擁有f(x)的閉包。這個時候就會有一種錯誤的感覺镐捧,g(x)捕捉了f(x)入?yún)⒌淖兞壳彼鳎瑥亩a(chǎn)生了不同的閉包臭增。從而得出g(x)不是純函數(shù)式的,因為它改變了狀態(tài)竹习。如果我們站在更高的層面去看待這個問題誊抛,函數(shù)在函數(shù)式編程里面是一等值,和結(jié)構(gòu)體整陌,整型拗窃,布爾類型沒有區(qū)別∶诒瑁回到上述的問題中來随夸,由于我們傳入了不同參數(shù),但是閉包里面的整體算法是沒有變化的震放。更加詳細(xì)的例子宾毒,f(x)返回一個計算x平方的函數(shù)g(x),g(x)雖然每次都會由f(x)傳入的x值變化而變化殿遂,但是g(x)整體算法就是計算x的平方诈铛,這個計算方法是沒有變化的,不根據(jù)外部狀態(tài)改變而改變的勉躺。那么這個g(x)的block是滿足函數(shù)式編程的不改變函數(shù)狀態(tài)的特性的癌瘾。所以它也是引用透明的。

額外需要說明的一點饵溅,__block這個關(guān)鍵字其實是破壞了函數(shù)式編程的妨退。

面試題:如何理解引用透明?

如果一個函數(shù)只會受到入?yún)⒌淖兓善螅敲催@個函數(shù)每次的調(diào)用都會是相同的
一個函數(shù)f(x),里面調(diào)用了g(x),g(x)里面又調(diào)用了h(x),h(x)最終計算出了結(jié)果咬荷,作為f(x)的返回值返回了。如果所有的狀態(tài)都沒有改變轻掩,f(x)下一次再調(diào)用相同的參數(shù)的時候幸乒,應(yīng)該會得到完全一樣的結(jié)果,那這個時候其實不用再調(diào)用g(x)和h(x)了,也可以得到完全一樣的結(jié)果唇牧。當(dāng)一個函數(shù)罕扎,不依賴“外部”變量和狀態(tài),只依賴入?yún)⒌淖兓绊懞瘮?shù)最終返回值丐重,也就是說入?yún)⑾嗤徽伲玫降姆祷刂到Y(jié)果一定相同,如果函數(shù)具有這種性質(zhì)扮惦,就可以說這個函數(shù)是引用透明的臀蛛。

typedef int(^intFx)(int a);

intFx transparent(intFx origin) {
    NSMutableDictionary *results = [NSMutableDictionary dictionary];
    return ^int(int p) {
        if (results[@(p)]) {
            return [results[@(p)] intValue];
        }
        results[@(p)] = @(origin(p));
        return [results[@(p)] intValue];
    };
}

在上述例子中可以看到,如果result里面有我們需要的值了,我們就不會再去調(diào)用回調(diào)的閉包浊仆,這樣transparent的函數(shù)每次傳入相同的值客峭,肯定會返回相同的結(jié)果。

一個純函數(shù)在執(zhí)行的過程中抡柿,只跟入?yún)⒂嘘P(guān)舔琅,在函數(shù)體中并不會引用外部全局變量,或者說是一個類方法里面的其他成員變量沙绝。另外搏明,純函數(shù)除了返回值之外鼠锈,也不會去改變外部的變量值闪檬。滿足上面這兩點的純函數(shù),就可以說它是引用透明的购笆。也有說法叫這種特性為冪等性

四.遞歸

函數(shù)式編程是用遞歸做為控制流程的機(jī)制粗悯。

五.只用"表達(dá)式",不用"語句"同欠,沒有副作用

"表達(dá)式"(expression)是一個單純的運算過程样傍,總是有返回值;"語句"(statement)是執(zhí)行某種操作铺遂,沒有返回值衫哥。函數(shù)式編程要求,只使用表達(dá)式襟锐,不使用語句撤逢。也就是說,每一步都是單純的運算粮坞,而且都有返回值蚊荣。
原因是函數(shù)式編程的開發(fā)動機(jī),一開始就是為了處理運算(computation)莫杈,不考慮系統(tǒng)的讀寫(I/O)互例。"語句"屬于讀寫操作,所以就被排斥在外筝闹。
函數(shù)式編程強(qiáng)調(diào)沒有"副作用"媳叨,意味著函數(shù)要保持獨立,所有功能就是返回一個新的值关顷,沒有其他行為糊秆,尤其是不得修改外部變量的值。

舉個例子來說明一下函數(shù)式編程和指令式編程的區(qū)別:


// 指令式編程
int factorial1(int x) {
    int result = 1;
    for (int i = 1; i <= x; i ++) {
        result *= i;
    }
    return result;
}

// 函數(shù)式編程
int factorial2(int x) {
    if (x == 1) return 1;
    return x * factorial2(x - 1);
}

上面這個例子就是計算階乘的例子解寝。我們先來看看指令式編程扩然。指令式編程,像機(jī)器一條條命令一樣思考問題聋伦。指令式的思想就類似于匯編夫偶,一條條指令告訴計算機(jī)該怎么去處理這個問題界睁。所以在指令式編程里面就有很多的狀態(tài)量語句。而在函數(shù)式編程里面兵拢,思想是利用數(shù)學(xué)方法來思考問題翻斟。階乘在數(shù)學(xué)定義里面就是f(n) = n * f(n - 1) (n > 1),f(n) = 1(n = 1)。在函數(shù)式編程里面是基本上沒有狀態(tài)量说铃,只有表達(dá)式访惜,也沒有賦值語句。利用了遞歸解決了問題腻扇。

再來看看指令式編程和響應(yīng)式編程的區(qū)別


void test() {
    int a = 5;
    int b = 8;
    int c = a + b;
    a = 10;
    NSLog(@"%d",c);
}

在指令式編程里面债热,計算是一種瞬間的操作。而響應(yīng)式編程幼苛,計算是相互相應(yīng)的窒篱,相互之間都存在關(guān)系,某些變化了舶沿,相互之間的關(guān)系會使相應(yīng)的值隨之變化墙杯。響應(yīng)式編程有2個典型的例子:Excel,當(dāng)單元格變化了括荡,相互之間的單元格也會立即變化高镐。Autolayout,當(dāng)父View變化了畸冲,根據(jù)相互之間的關(guān)系Constraint嫉髓,子View的frame也會隨之變化。

在面向?qū)ο笳Z言中也是可以實現(xiàn)響應(yīng)式編程的召夹,具體做法應(yīng)該是岩喷,把關(guān)系抽象出來,然后把變化抽象出來监憎,用關(guān)系把變化事件傳遞下去纱意。Cocoa框架下RAC的實現(xiàn)就是如此。

最后再來說說函數(shù)響應(yīng)式編程鲸阔。
首先函數(shù)響應(yīng)式編程肯定是滿足函數(shù)式編程的上述特性的偷霉。函數(shù)響應(yīng)式編程是面向離散事件流的,在一個時間軸上會產(chǎn)生一些離散事件褐筛,這些事件會依次向下傳遞类少。

RAC就是Cocoa框架下的函數(shù)響應(yīng)式編程的實現(xiàn)。它提供了基于時間變化的數(shù)據(jù)流的組合和變化渔扎。

接著再來說說之前說的4種編程范式硫狞,總結(jié)出來,如果按照類似繼承圖譜來看的話,應(yīng)該如下圖:


首先在聲明式編程里面有2大家族残吩,那就是函數(shù)式編程和數(shù)據(jù)流編程财忽,數(shù)據(jù)流編程下面就是響應(yīng)式編程,而函數(shù)響應(yīng)式編程是"繼承"于函數(shù)式編程和響應(yīng)式編程的泣侮。

面向?qū)ο缶幊叹蛯儆谥噶钍骄幊痰姆懂牸幢搿纳厦?張圖來看,我們可以很明顯看出這4者是什么關(guān)系了活尊。

面試題:函數(shù)式編程是面向?qū)ο缶幊痰纳壆a(chǎn)品
由上面的說明來看隶校,這個說法肯定是錯誤的,關(guān)系根據(jù)上面2圖來看就很明顯了蛹锰。

面試題:函數(shù)式語言主張不變量的原因是什么深胳?

  1. 函數(shù)保持獨立,所有功能就是返回一個新的值宁仔,沒有其他行為稠屠,尤其是不修改外部變量的值。由于這一主張翎苫,我們不需要考慮線程"死鎖"問題,線程之間一定是安全的榨了,因為它不修改變量煎谍,所以根本不存在"鎖"線程的問題。
  2. 進(jìn)一步龙屉,函數(shù)式語言更加趨向于數(shù)學(xué)公式的推導(dǎo)叉瘩,在數(shù)學(xué)公式里面其實是完全不存在變量這一概念的剃诅,此時如果又不存在變量了,那整個程序的執(zhí)行順序其實就不必要了,這樣可以使我們更加容易的進(jìn)行并發(fā)編程安券,更加有效率的利用多核cpu的計算處理能力。

二.鏈?zhǔn)秸{(diào)用

定義:f(x)猛频,表示的是一種態(tài)射刊咳,從x的定義域到f(x)值域的態(tài)射。如果定義域和值域是完全相同的話枢步,這種映射也成為單元態(tài)射沉删。那么滿足單元態(tài)射的函數(shù),就可以進(jìn)行鏈?zhǔn)秸{(diào)用醉途。

以RAC為例矾瑰,把RACSignal鏈?zhǔn)絺鬟f下去,subscribeNext就會返回一個RACSignal隘擎,定義域和值域都是RACSignal殴穴,那么就滿足了單元態(tài)射的要求,就可以鏈?zhǔn)秸{(diào)用下去。

面試題:組成鏈?zhǔn)秸{(diào)用的必要條件就是在方法里面返回對象自己

這個說法是錯誤采幌,舉個例子:RAC每次做信號變換的時候恍涂,都產(chǎn)生了一個新的信號,所以返回自己就并不是必要條件植榕。其實如果返回自己的同類或者和自己類似的類型再沧,里面也包含可以繼續(xù)鏈?zhǔn)秸{(diào)用的方法,也是可以組成鏈?zhǔn)秸{(diào)用的尊残。

三.關(guān)于RAC的其他一些概念

面試題:ReactiveCocoa是Facebook出的一個FRP開源庫

錯誤炒瘸,是寫Github客戶端時候的附屬品,附帶開發(fā)出的一個開源框架寝衫。

面試題:**ReactiveCocoa是基于KVO的一個開源庫 **

錯誤顷扩。KVO是RAC非常次要的部分,甚至可以說沒有KVO慰毅,RAC依舊可以存在隘截。

面試題:**ReactiveCocoa是一個純函數(shù)式編程的庫 **

錯誤,由于Cocoa框架并不是函數(shù)式汹胃,RAC又是在Cocoa框架下婶芭,所以就不是純函數(shù)式。在命令式編程的語言范疇里面實現(xiàn)純函數(shù)編程着饥,需要折中的方法犀农,我們可以封裝命令式編程,使其向上層可以形成純函數(shù)式的宰掉,但是下層肯定就是命令式編程實現(xiàn)的呵哨。

最后我們再來區(qū)分一個概念:

面試題:RAC中Pull-driver和Push-driver的區(qū)別?

Pull-driver是指的是任何時刻轨奄,我們?nèi)绻枰獢?shù)據(jù)了孟害,都可以從pull-driver里面拿走數(shù)據(jù),因為數(shù)據(jù)先存儲了挪拟。整個取數(shù)據(jù)的時間控制在調(diào)用者手上挨务。典型的例子就是for-in循環(huán),這就是一個pull-driver的操作舞丛。不管你循環(huán)幾次耘子,每次循環(huán)如何操作,數(shù)組或者字典里面的數(shù)據(jù)都一直存在在那里球切,“躺”在那里谷誓。
Push-driver是相反的,在任何時刻吨凑,當(dāng)有數(shù)據(jù)或者事件產(chǎn)生捍歪,都會push給你户辱,如果你此時沒有處理,該事件或者數(shù)據(jù)就丟失了糙臼。整個取數(shù)據(jù)的時間并不控制在調(diào)用者的手里庐镐。

Pull-driver可以類比看書,知識和文字不管你看不看变逃,一直都在書里必逆。
Push-driver可以類比看電視,節(jié)目不管你看不看揽乱,都一直播放名眉,你錯過了就是錯過了。

在RAC里面凰棉,Sequence就是一個pull-driver损拢,Signal就是一個push-driver。

未完待續(xù)……

我會不定期把關(guān)于RAC相關(guān)難理解易混淆的概念都整理進(jìn)來……歡迎大家指點撒犀。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末福压,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子或舞,更是在濱河造成了極大的恐慌荆姆,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,042評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件嚷那,死亡現(xiàn)場離奇詭異胞枕,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)魏宽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評論 2 384
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來决乎,“玉大人队询,你說我怎么就攤上這事」钩希” “怎么了蚌斩?”我有些...
    開封第一講書人閱讀 156,674評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長范嘱。 經(jīng)常有香客問我送膳,道長,這世上最難降的妖魔是什么丑蛤? 我笑而不...
    開封第一講書人閱讀 56,340評論 1 283
  • 正文 為了忘掉前任叠聋,我火速辦了婚禮,結(jié)果婚禮上受裹,老公的妹妹穿的比我還像新娘碌补。我一直安慰自己虏束,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,404評論 5 384
  • 文/花漫 我一把揭開白布厦章。 她就那樣靜靜地躺著镇匀,像睡著了一般。 火紅的嫁衣襯著肌膚如雪袜啃。 梳的紋絲不亂的頭發(fā)上汗侵,一...
    開封第一講書人閱讀 49,749評論 1 289
  • 那天,我揣著相機(jī)與錄音群发,去河邊找鬼晰韵。 笑死,一個胖子當(dāng)著我的面吹牛也物,可吹牛的內(nèi)容都是我干的宫屠。 我是一名探鬼主播,決...
    沈念sama閱讀 38,902評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼滑蚯,長吁一口氣:“原來是場噩夢啊……” “哼浪蹂!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起告材,我...
    開封第一講書人閱讀 37,662評論 0 266
  • 序言:老撾萬榮一對情侶失蹤坤次,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后斥赋,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體缰猴,經(jīng)...
    沈念sama閱讀 44,110評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年疤剑,在試婚紗的時候發(fā)現(xiàn)自己被綠了滑绒。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,577評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡隘膘,死狀恐怖疑故,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情弯菊,我是刑警寧澤纵势,帶...
    沈念sama閱讀 34,258評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站管钳,受9級特大地震影響钦铁,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜才漆,卻給世界環(huán)境...
    茶點故事閱讀 39,848評論 3 312
  • 文/蒙蒙 一牛曹、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧栽烂,春花似錦躏仇、人聲如沸恋脚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽糟描。三九已至,卻和暖如春书妻,著一層夾襖步出監(jiān)牢的瞬間船响,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評論 1 264
  • 我被黑心中介騙來泰國打工躲履, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留见间,地道東北人。 一個月前我還...
    沈念sama閱讀 46,271評論 2 360
  • 正文 我出身青樓工猜,卻偏偏與公主長得像米诉,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子篷帅,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,452評論 2 348

推薦閱讀更多精彩內(nèi)容