【原創】FRP初探(函數式編程部分)

前言

我之前上學時和工作中所接觸的編程語言,C++、Java、Objective-C,全部都是面向對象的語言,直到學習了Swift。

通過學習和在App中的實踐,感覺Swift跟我們以前常用的Objective-C不太一樣,蘋果雖然把它定義成一個Protocol-Oriental的語言,但它實際上更像是一個多范式的語言,我們可以用它來做一些之前不能做或者不太方便做的事情,比如Functional、Reactive等等這些范式。尤其是當我查閱一些大牛寫的金光閃閃的源碼的時候,發現很多的都是FRP的,而且他們的API也跟Objective-C相比有了很大的變化,之所以會有很大變化,是因為語言本質上是不同的。雖然用Swift或者Objective-C都是在寫iOS的App,但是當語言發生變化的時候,如果我們的思維沒有發生變化,那么我們的思維就是落后于語言的,我們只不過是用Swift在寫Objective-C的代碼而已。

FRP是最近很火的一個詞,很多業內大神都在說這個詞,把它描繪成一個很NB的東東,但跟我們有什么關系呢?

比如我們說函數式編程,就要說高階函數,就是一個函數可以當做一個值,可以作為函數的參數,也可以作為函數的返回值。或者我們說,函數式編程就是用組合的方式,把很多小函數組合成一個非常NB的函數,然后一次性幫你解決所有問題。或者是柯里化,不可變狀態,引用透明,惰性求值,遞歸,等等等等這些函數式特性。

我在學習函數響應式編程的時候充滿了好奇,尤其是它的一些變體,比如Rx系列,RAC等等。但真正學習起來,發現學習函數響應式編程其實還是挺難的,尤其是缺少好的資料的時候。很多資料都是在介紹函數響應式編程如何如何好,或者是介紹各種Rx庫該如何使用。

其實學習過程中,最難的部分是如何以函數響應式的方式來思考,更多的意味著要摒棄那些老舊的命令式和狀態式的典型編程習慣,并且強迫自己的大腦以不同的方式來運作,但是網上很少有這樣的教程文章。

什么是函數響應式編程

我查閱了wikipedia中關于函數響應式編程的解釋:

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).

函數響應式編程是一個使用函數式編程(例如map,reduce,filter)構建的響應式編程(異步數據流編程)的編程范式。

In computing, reactive programming is an asynchronous programming paradigm concerned with data streams and the propagation of change.

在計算中,響應式編程是一種與數據流和變更傳播有關的異步編程范式。

In computer science, functional programming is a programming paradigm—a style of building the structure and elements of computer programs—that treats computation as the evaluation of mathematical functions and avoids changing-state and mutable data.

在計算機科學中,函數式編程是一種編程范式,它是一種構建計算機程序結構和元素的方式,它將計算視為對數學函數的評估,并避免了變化狀態和可變數據。

看完之后有種暈暈的感覺,翻來覆去就說的是那些東東,而且我們注意到,這幾段解釋都提到了一個詞:paradigm,翻譯作范式。那什么又是范式呢?

范式

Paradigm.png

這些是常見的編程范式和它們的關系。Reactive Programming也是一種聲明式的編程范式,“繼承”自Dataflow Programming。它認為說,一個應用的組織形式,應該是針對那些數據流做一些處理和響應。當結合了函數式和響應式編程的特點,就產生了Functional Reactive Programming這個東西。

等等,到這里說了這么多,還是沒有解釋明白到底什么是范式。

我理解的范式,是一種思維模式,也就是THINKING。

為什么這么說呢?當我們在用面向對象編程的時候,其實我們是在說,我把我的世界認為是類和對象的世界,對象有某個特定的類型,對象和對象之間有某種聯系,對象也有特定的行為。經典的Java、C++、Objective-C,都是這種形式,于是我們的程序就跑起來了,界面就產生了。它的核心就是說:我把我們的程序世界認為是有很多很多對象,并且他們相互作用的一個世界,所以我們說這是面向對象編程,是我們的一種思維模式。

Functional Programming也一樣是一種思維模式,它認為我們一個程序是一個求值過程,我們拿到一個值,可以對它進一步求值,就產生了一個個的function。它認為一個程序世界是由function組成的世界,我們們把一個數據從源頭輸入進去,經過一個一個function處理并向下傳遞,像一個個數據管道接起來一樣流過去。

其實我認為就模塊復用性上來說,函數式編程要強于面向對象編程。因為面向對象的本質,是把數據和行為(屬性和方法)打包在一起,組成類和對象,它通過繼承和抽象,給我們提供多態的行為(面向對象的核心是多態)。函數式編程,是把一個個的函數像鏈條一樣組合起來,這種思維模式使得你需要去用更細粒度去做抽象和模塊化。

函數式編程

Talk is cheap, show me the code. 那么我們就先從Hello world的代碼開始看起。

3.times { print("hello world") }

一個不會編程的人看到這段代碼,肯定會知道它的意思是把“hello world”打印3遍。相比于指令式的for循環,可讀性和逼格都一下子增強了好幾個等級。

指令式編程,其實是讓我們人像一個機器一樣去思考。為什么這樣說?看下面這段代碼。

let nums = [1, 2, 3, 4, 5, 6, 7]
var strs = [String]()
for var i = 0; i < nums.count; i++ {
    strs.append(String(nums[i]))
}

這段代碼,其實就是把我們的思維映射到了CPU模型上。一個機器工作的時候,它也需要開辟一塊內存,然后不斷變更一個寄存器里的值,通過這個值的遞增做循環,然后把原數據從原數組中取出,開辟字符串的內存并存入指定的值,然后插入到空數組里。其實在寫這段代碼的時候,我們的思維都是像CPU一樣地去工作,但是當我們寫多了之后,會覺得這很自然。但真的是這樣嗎?

作為一個程序員,其實我們可能更希望嘗試像一個人一樣去思考,所以我們來看看聲明式的編程:

let nums = [1, 2, 3, 4, 5, 6, 7]
let strs = nums.map(String.init)

且不說代碼行數比之前短了很多,最重要的是我們的思維模式產生了變化,我們可以用人類的思考方式去解決這個問題。我們從原來的面相CPU面相機器編程,變成了函數式聲明式的編程,我們告訴編譯器,我要一個字符串數組,它是從一個int數組映射過來的。這個映射關系其實就是函數式編程的本質,或者說是思維源頭。當我們這么做的時候,其實我們是在做數學,而數學是人類發明出的一個抽象工具,來把宇宙的所有東西想要框定進去(雖然數學可能做不到,但人類沒有比數學更高級的抽象工具了)。

你肯定覺得還不夠,那么再來一個例子,我們就說面試中經常提到的快速排序。我們都知道快速排序的原理:取一個基準值,將比基準值小的值放在基準值左邊,將比基準值大的值放在基準值右邊,再對左右兩部分各自遞歸。好了,我們先看看C++實現:

void qsort(T lst[], int head, int tail) {    
    if (head >= tail) return ;

    int i = head, j = tail;

    T pivot = lst[head];  // 通常取第一個數為基準

    while (i < j) { // i,j 相遇即退出循環
        while (i < j && lst[j] >= pivot) j--;
        lst[i] = lst[j];    // 從右向左掃描,將比基準小的數填到左邊
        while (i < j && lst[i] <= pivot) i++;
        lst[j] = lst[i];    //  從左向右掃描,將比基準大的數填到右邊
    }

    lst[i] = pivot; // 將 基準數 填回

    qsort(lst, head, i - 1);    // 以基準數為界左右分治
    qsort(lst, j + 1, tail);
}

我相信你在看這段代碼的時候肯定和我一樣,腦海中出現了兩個指針i和j,一個指向數組頭,一個指向數組尾,指向數組頭的指針往右移,指向數組尾的指針往左移,然后balabala……
我們看看聲明式的代碼是什么樣的:

extension Array where Element: Comparable {
    func quickSort() -> Array<Element> {
        guard self.count >= 2 else {
            return self
        }
        let base = self[0]
        let lesser = self.filter { $0 < base }
        let equal = self.filter { $0 == base }
        let greater = self.filter { $0 > base }
        return lesser.quickSort() + equal + greater.quickSort()
    }
}

不僅僅是不需要任何注釋,從代碼里就讀出了快速排序的思路,更關鍵的是,我們終于可以直接使用人腦的思維寫出了這樣的代碼,而不是再以CPU的思維來寫代碼了。

更高級的抽象

函數式帶給我們的,其實是一種更加抽象的封裝。比如Swift中的Array、Dictionary、Optional等等這些容器類型,都map和flatMap方法。當我們對它們進行map的時候,它門內部都是對容器內部的值去進行計算(根據參數傳入的函數進行計算),它的抽象并不是抽象出一堆的數據結構,不是抽象出一堆狀態或方法,它抽象的是一個計算過程,也就是說我們可以對這個容器這個值進行任意計算。

下面我們看一個常見問題,通常在處理異步回調的時候會這么寫:

// Async callback
(value: T?, error: ErrorType?) -> Void
if let error = error {
    // handle error
} else if let value = value {
    // handle value
} else {
    // all nil?
}
// all non nil?

顯然它的API設計顯得有些煩人,所以我們定義ResultType解決這個問題。

// 定義Result解決這個問題
enum Result<Value> {
    case Failure(ErrorType)
    case Success(Value)
}
  
(result: Result<T>) -> Void
switch result {
case let .Error(error):
    // handle error
case let .Success(value):
    // handle value
}

ResultType已經幫助我們解決了很棘手的問題,但其實它還可以提供給我們更強大更抽象的東西:實現map和flatMap。

enum Result<Value> {
    func flatMap<T>(transform: Value -> Result<T>) -> Result<T> {
        switch self {
        case let .Failure(error):
            return .Failure(error)
         
        case let .Success(value):
            return transform(value)
        }
    }
  
    func map<T>(transform: Value -> T) -> Result<T> {
        return flatMap { .Success(transform($0)) }
    }
}

看看它是如何強大的。

// 根據data生成圖片
func toImage(data: NSData) -> Result<UIImage>
  
// 給圖片設置alpha
func addAlpha(image: UIImage) -> Result<UIImage>
  
// 給圖片切割圓角
func roundCorner(image: UIImage) -> Result<UIImage>
  
// 給圖片做模糊處理
func applyBlur(image: UIImage) -> Result<UIImage>
  

// 基于ResultType的鏈式編程
toImage(data)
    .flatMap {
        return addAlpha
    }.flatMap {
        return roundCorner
    }.flatMap {
        return applyBlur
    }

不知道大家發現了沒有,如果這里面把flatMap函數的名字改成then,簡直是像極了JavaScript中赫赫有名的Promise。對的,Promise和我們定義的ResultType的基本原理一樣,都是Monad(單子)。

Monad

Monad是一個可怕的名詞,為什么說它可怕?google一下得到的解釋是:

一個自函子范疇上的幺半群

是不是一個頭已經兩個大了,你說可怕不可怕?

在程序員界,讓人害怕最多的可能就是兩個詞:指針和Monad,而指針和Monad實際上都是一個高級抽象的過程。

好了,不嚇人了,拋開那些難懂的概念,簡單地說,Monad其實就是一個容器,實現了那兩個方法:map和flatMap。

比如我們上面說了,Swift中Array、Dictionary、Optional等等這些容器類型,都map和flatMap方法,所以他們都是Monad。
比如PromiseKit,它可以把異步計算的結果給封裝在一個數據里面,等到這個值真正產生的時候,就可以拿出結果。Promise的核心方法:兩個then,跟我們Array中的flatMap、map完全沒有區別,Promise也是一個Monad。
再比如Reative Programming,它能夠讓我們對Observable做一些計算、封裝等等。其實它里面一系列的方法,都是這樣的:當我們去observe,combine的時候,就是拿到一個Observable對象,傳進去一個閉包,對它里面擁有的值去進行一些操作,然后返回另外一個Observable。所有的這些,其實就是把Reactive的概念(可序列化、可響應的值)用Monad的形式封裝起來,提供給我們一個對計算過程的抽象,我們就可以基于它來做一些流式的開發。

Monad幫我們把計算過程抽象出來,同時當出現任何錯誤的時候,沒有任何額外多余的計算步驟,直接把錯誤返回。

小結

函數式編程給我們的,是對計算的更高級的抽象,當我去學習嘗試各種函數式編程技巧,這些技巧不是最重要的,最重要的是我們的思維會得到改變。

下一次分享,我會更加詳細地介紹Monad和她的姐妹:Functor、Applicative。

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

推薦閱讀更多精彩內容