前言
我之前上學時和工作中所接觸的編程語言,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,翻譯作范式。那什么又是范式呢?
范式
這些是常見的編程范式和它們的關系。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。