【原創(chuàng)】FRP初探(函數(shù)式編程部分)

前言

我之前上學(xué)時和工作中所接觸的編程語言,C++、Java、Objective-C,全部都是面向?qū)ο蟮恼Z言,直到學(xué)習(xí)了Swift。

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

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

比如我們說函數(shù)式編程,就要說高階函數(shù),就是一個函數(shù)可以當(dāng)做一個值,可以作為函數(shù)的參數(shù),也可以作為函數(shù)的返回值。或者我們說,函數(shù)式編程就是用組合的方式,把很多小函數(shù)組合成一個非常NB的函數(shù),然后一次性幫你解決所有問題?;蛘呤强吕锘豢勺儬顟B(tài),引用透明,惰性求值,遞歸,等等等等這些函數(shù)式特性。

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

其實(shí)學(xué)習(xí)過程中,最難的部分是如何以函數(shù)響應(yīng)式的方式來思考,更多的意味著要摒棄那些老舊的命令式和狀態(tài)式的典型編程習(xí)慣,并且強(qiáng)迫自己的大腦以不同的方式來運(yùn)作,但是網(wǎng)上很少有這樣的教程文章。

什么是函數(shù)響應(yīng)式編程

我查閱了wikipedia中關(guān)于函數(shù)響應(yīng)式編程的解釋:

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

函數(shù)響應(yīng)式編程是一個使用函數(shù)式編程(例如map,reduce,filter)構(gòu)建的響應(yīng)式編程(異步數(shù)據(jù)流編程)的編程范式。

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

在計(jì)算中,響應(yīng)式編程是一種與數(shù)據(jù)流和變更傳播有關(guān)的異步編程范式。

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.

在計(jì)算機(jī)科學(xué)中,函數(shù)式編程是一種編程范式,它是一種構(gòu)建計(jì)算機(jī)程序結(jié)構(gòu)和元素的方式,它將計(jì)算視為對數(shù)學(xué)函數(shù)的評估,并避免了變化狀態(tài)和可變數(shù)據(jù)。

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

范式

Paradigm.png

這些是常見的編程范式和它們的關(guān)系。Reactive Programming也是一種聲明式的編程范式,“繼承”自Dataflow Programming。它認(rèn)為說,一個應(yīng)用的組織形式,應(yīng)該是針對那些數(shù)據(jù)流做一些處理和響應(yīng)。當(dāng)結(jié)合了函數(shù)式和響應(yīng)式編程的特點(diǎn),就產(chǎn)生了Functional Reactive Programming這個東西。

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

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

為什么這么說呢?當(dāng)我們在用面向?qū)ο缶幊痰臅r候,其實(shí)我們是在說,我把我的世界認(rèn)為是類和對象的世界,對象有某個特定的類型,對象和對象之間有某種聯(lián)系,對象也有特定的行為。經(jīng)典的Java、C++、Objective-C,都是這種形式,于是我們的程序就跑起來了,界面就產(chǎn)生了。它的核心就是說:我把我們的程序世界認(rèn)為是有很多很多對象,并且他們相互作用的一個世界,所以我們說這是面向?qū)ο缶幊?,是我們的一種思維模式。

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

其實(shí)我認(rèn)為就模塊復(fù)用性上來說,函數(shù)式編程要強(qiáng)于面向?qū)ο缶幊?。因?yàn)槊嫦驅(qū)ο蟮谋举|(zhì),是把數(shù)據(jù)和行為(屬性和方法)打包在一起,組成類和對象,它通過繼承和抽象,給我們提供多態(tài)的行為(面向?qū)ο蟮暮诵氖嵌鄳B(tài))。函數(shù)式編程,是把一個個的函數(shù)像鏈條一樣組合起來,這種思維模式使得你需要去用更細(xì)粒度去做抽象和模塊化。

函數(shù)式編程

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

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

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

指令式編程,其實(shí)是讓我們?nèi)讼褚粋€機(jī)器一樣去思考。為什么這樣說?看下面這段代碼。

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]))
}

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

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

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

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

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

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

    int i = head, j = tail;

    T pivot = lst[head];  // 通常取第一個數(shù)為基準(zhǔn)

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

    lst[i] = pivot; // 將 基準(zhǔn)數(shù) 填回

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

我相信你在看這段代碼的時候肯定和我一樣,腦海中出現(xiàn)了兩個指針i和j,一個指向數(shù)組頭,一個指向數(shù)組尾,指向數(shù)組頭的指針往右移,指向數(shù)組尾的指針往左移,然后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()
    }
}

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

更高級的抽象

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

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

// 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設(shè)計(jì)顯得有些煩人,所以我們定義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已經(jīng)幫助我們解決了很棘手的問題,但其實(shí)它還可以提供給我們更強(qiáng)大更抽象的東西:實(shí)現(xiàn)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)) }
    }
}

看看它是如何強(qiáng)大的。

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

// 基于ResultType的鏈?zhǔn)骄幊?toImage(data)
    .flatMap {
        return addAlpha
    }.flatMap {
        return roundCorner
    }.flatMap {
        return applyBlur
    }

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

Monad

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

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

是不是一個頭已經(jīng)兩個大了,你說可怕不可怕?

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

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

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

Monad幫我們把計(jì)算過程抽象出來,同時當(dāng)出現(xiàn)任何錯誤的時候,沒有任何額外多余的計(jì)算步驟,直接把錯誤返回。

小結(jié)

函數(shù)式編程給我們的,是對計(jì)算的更高級的抽象,當(dāng)我去學(xué)習(xí)嘗試各種函數(shù)式編程技巧,這些技巧不是最重要的,最重要的是我們的思維會得到改變。

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容