開個新坑,來寫寫用 Swift 來做函數(shù)式編程的技巧。
引言:任何一個沾點(diǎn)
Functional
特性的框架,比如Promise,ReactiveCocoa或者RxSwift都提供了處理異步操作順序執(zhí)行的方案。
但其實(shí)用 Swift 本身自帶很多Functional的特性,自己實(shí)現(xiàn)也并不難。
本文探討了其中一種方法。
提起異步操作的序列執(zhí)行,指的是有一系列的異步操作(比如網(wǎng)絡(luò)請求)的執(zhí)行有前后的依賴關(guān)系,前一個請求執(zhí)行完畢后,才能執(zhí)行下一個請求。
異步操作的定義
我們定義一般異步操作都是如下形式:
func asyncOperation(complete : ()-> Void){
//..do something
complete()
}
常規(guī)的異步操作都會接受一個閉包作為參數(shù),用于操作執(zhí)行完畢后的回調(diào)。
那異步操作的序列化會有什么問題呢? 看如下的偽代碼:
func asyncOperation(complete : ()-> Void){
//..do something
print("fst executed")
complete()
}
func asyncOperation1(complete : ()-> Void){
//..do something
print("snd executed")
complete()
}
func asyncOperation2(complete : ()-> Void){
//..do something
print("third executed")
complete()
}
我們定義了三個操作asyncOperation
,asyncOperation1
和 asyncOperation2
,現(xiàn)在我們想序列執(zhí)行三個操作,然后在執(zhí)行完后輸出 all executed
。 按照常規(guī),我們就寫下了如下的代碼:
asyncOperation {
asyncOperation1({
asyncOperation2({
print("all executed")
})
})
}
可以看到,明明才三層,代碼似乎就有點(diǎn)復(fù)雜了,而我們真正關(guān)心的代碼卻只有 print("all executed")
這一行。但為了遵從前后依賴的時(shí)許關(guān)系,我們不得不小心的處理回調(diào),以防搞錯層級。如果層級多了就有可能像這樣:
asyncOperation {
asyncOperation1({
asyncOperation2({
asyncOperation3{
asyncOperation4{
asyncOperation5{
print("all executed")
}
}
}
})
})
}
這就是傳說中的callback hell
, 而且這還只是最clean的情況,實(shí)際情況中還會耦合很多的邏輯代碼,更加無法維護(hù)。
用reduce來實(shí)現(xiàn)異步操作的串行
那是否有解決辦法呢? 答案是有的。很多FRP的框架都提供了類似的實(shí)現(xiàn),有興趣的讀者可以自行查看Promise、 ReactiveCocoa 和 RxSwift中的實(shí)現(xiàn)。
然后正如本節(jié)的標(biāo)題所說,Swift提供了兩個函數(shù)式的特性:
- 函數(shù)是一等公民(可以像變量一樣傳來傳去,可以做函數(shù)參數(shù)、返回值
- 高階函數(shù),比如
map
和reduce
接下來我們就用這兩個特性,實(shí)現(xiàn)一個更加優(yōu)雅的方式來做異步操作串行。
1. 定義類型
為了方便書寫,我們先定義一下異步操作的類型:
typealias AsyncFunc = (()->Void) -> Void
AsyncFunc 代表了一個函數(shù)類型,這樣的函數(shù)有一個閉包參數(shù)(其實(shí)就是上面 asyncOperation 的類型)
2. 從串行兩個操作開始
我們先化簡問題,假設(shè)我們只需要串行兩個異步操作呢? 有沒有辦法把兩個異步操作串行成一個異步操作呢? 想到這里,我們可以YY出這樣一個函數(shù):
func concat(left : AsyncFunc , right : AsyncFunc) -> AsyncFunc{
}
concat函數(shù),顧名思義,是連接的意思。指的是將兩個異步操作:left
和right
串行起來,并返回一個新的異步操作。
那現(xiàn)在,我們來思考如何實(shí)現(xiàn)concat
函數(shù),既然返回的是AsyncFunc
也就是一個函數(shù),那我們可以先YY出這樣的結(jié)構(gòu):
func concat(left : AsyncFunc , right : AsyncFunc) -> AsyncFunc{
return { complete in
}
}
仔細(xì)回憶 AsyncFunc
的類型: (()->Void) -> Void
,所以閉包參數(shù)complete
就對應(yīng)前面的參數(shù)。
架子已經(jīng)寫好了,我們來思考要實(shí)現(xiàn)如何實(shí)現(xiàn)最終返回這個函數(shù)。根據(jù)concat的定義我們可以知道,我們最終返回的是一個 接受一個閉包作為參數(shù), 先執(zhí)行l(wèi)eft,成功后執(zhí)行right,成功后再執(zhí)行傳入的閉包。
你看,這樣一分析,邏輯就非常清晰了,閉包參數(shù)就是complete
. 我們抽絲剝繭,找到了問題的本質(zhì),于是很容易可以寫出:
func concat(left : AsyncFunc , right : AsyncFunc) -> AsyncFunc{
return { complete in
left{
right{
complete()
}
}
}
}
核心邏輯和我們最原始的版本其實(shí)并沒有區(qū)別,區(qū)別就是不論再多個串行,我們都不需要寫更多的嵌套了。
基于最開始的例子,我們測試一下:
let concatedFunction = concat(asyncOperation,
right: asyncOperation1)
concatedFunction {
print("all executed")
}
至此,我們以及成功的實(shí)現(xiàn)了把兩個異步操作合并成一個串行的異步操作。
3. 定義一個運(yùn)算符
讓我們回過頭去,再審視一個我們concact的簽名:
func concat(left : AsyncFunc , right : AsyncFunc) -> AsyncFunc{
我們忘記什么函數(shù),什么閉包,什么異步。就來看簽名:他接收兩個相同類型的參數(shù),最后返回一個結(jié)果,結(jié)果的類型和參數(shù)一致。
像什么?像霧像雨又像風(fēng)? 還是像加法像減法又像乘法?總之我們可以把他看做是某種運(yùn)算,具備如下性質(zhì):
a -> b -> c = concact(a,b) -> c = concat( concat(a,b) , c) (-> 代表異步地串行執(zhí)行)
既然是運(yùn)算,我們干脆給他定義個運(yùn)算符,修改我們的concat
函數(shù)如下所示, + 代表這是一種用來表示結(jié)合的運(yùn)算,>代表他有前后的依賴關(guān)系,不滿足交換律。+>
就是我們自己定義的異步串行運(yùn)算符。
infix operator +> {associativity left precedence 150}
func +> (left:AsyncFunc,right:AsyncFunc) -> AsyncFunc{
return { complete in
left{
right{
complete()
}
}
}
}
這樣,我們最開始的,五個異步操作串行執(zhí)行的代碼就可以改為這樣:
let concatedFunction = asyncOperation +>
asyncOperation1 +>
asyncOperation2 +>
asyncOperation3 +>
asyncOperation4 +>
asyncOperation5
concatedFunction {
print("all executed")
}
我們先把五個操作串行成一個,然后執(zhí)行它。
4. 串行任意多個異步操作
那你會說,如果我們有更多的異步操作呢?比如我們有一組異步操作:[AsyncFunc]
, 難道只能展開來一個個用 +>
來合并嗎?
其實(shí),現(xiàn)在我們有了串行運(yùn)算符,那就很容易想到我們可以拿我們剛才實(shí)現(xiàn)的+>
運(yùn)算符來reduce一組異步操作。繼續(xù)用剛才的例子,我們先寫下如下代碼:
let reducedFunction = [asyncOperation,asyncOperation1,asyncOperation2].
reduce(【初始值】, combine: +>)
我們把剛才定義的三個異步函數(shù)扔到列表里,然后用我們的串行運(yùn)算符+>
來reduce他,combine 其實(shí)就是+>
,但此時(shí)似乎又面臨另外一個問題,【初始值】填什么?
每次思考reduce
的初始值都是一個哲學(xué)問題,大多數(shù)情況下我們不希望他參與運(yùn)算,但又不得不讓他參與運(yùn)算(因?yàn)閏ombine是個二元函數(shù)),所以我們希望reduce的初始值(記為initial
)具備如下性質(zhì):
combine(initial,x) = x
這種性質(zhì),大家應(yīng)該能聯(lián)想到一個類似的東西叫 CGAffineTransformIdentity
,往深了講,這其實(shí)是一個代數(shù)問題,不過這里暫時(shí)不討論。
在本例,我們的initial
可以定義為:
let identityFunc:AsyncFunc = {f in f()}
它是這樣的一個函數(shù),接受閉包作為參數(shù),然后什么都不做,馬上調(diào)用閉包。這里大家簡單感受一下。_(:зゝ∠)
于是,我們完整的reduce版本可以定義為:
let identityFunc:AsyncFunc = {f in f()}
let reducedFunction = [asyncOperation,asyncOperation1,asyncOperation2,asyncOperation3,asyncOperation4,asyncOperation5].
reduce(identityFunc, combine: +>)
reducedFunction {
print("all executed")
}
首先定義了identityFunc
作為初始值,然后把我們開頭定義的幾個異步操作reduce成一個:reducedFunction
,然后調(diào)用了它,可以觀察輸出結(jié)果,和我們最開始寫的嵌套版本是一樣的。
引申的話題
帶參數(shù)的串行
真實(shí)世界里,當(dāng)我們需要串行異步操作的時(shí)候,一般后一個操作都需要前一個操作的執(zhí)行結(jié)果。比如我們可能需要先請求新聞的列表,拿到新聞的id之后,再請求新聞的一些具體的信息,前后操作有數(shù)據(jù)上的依賴關(guān)系。(當(dāng)然一般不這么搞,這里只是舉個例子)
抽象的來看,我們要處理一組串行的操作,為了方便處理,我們希望函數(shù)的簽名是一樣的,偷懶的做法可以這樣:
typealias AsyncFunc = (info : AnyObject,complete:(AnyObject)->Void) -> Void
定義閉包的類型為AnyObject->Void
,同時(shí)異步函數(shù)也接受一個AnyObject
的參數(shù),這樣在各個異步函數(shù)中通過把參數(shù)cast成字典,提取信息,操作完畢后把結(jié)果的值傳到回調(diào)的閉包中。具體實(shí)現(xiàn)見一下節(jié)
如果嫌AnyObject
太丑的話也可以針對串行操作的場景設(shè)計(jì)一個protocol,然后用protocol作為參數(shù)的類型來傳遞信息。
錯誤處理
我們最終將一組異步操作,reduce成了一個異步操作,那如果中間某個操作出錯了,我們該怎么知道呢? 其中一種實(shí)現(xiàn),可以是:
typealias AsyncFunc = (info : AnyObject,complete:(AnyObject?,NSError?)->Void) -> Void
對比之前帶參數(shù)的例子,唯一的區(qū)別就是在閉包的參數(shù)里加了一個NSError?,以及把AnyObject改成了optional,因?yàn)檫@里的AnyObject代表的是結(jié)果,如果失敗了,結(jié)果自然就是nil.
于是,我們的核心,串行運(yùn)算符可以變成這樣:
func +>(left : AsyncFunc , right : AsyncFunc) -> AsyncFunc{
return { info , complete in
left(info: info){ result,error in
guard error == nil else{
complete(nil,error)
return
}
right(info: info){result,error in
complete(result,error)
}
}
}
}
邏輯也是很直接的,我們首先嘗試執(zhí)行l(wèi)eft,在left的回調(diào)中查看error是否是nil,如果不是,說明有錯誤,則立刻執(zhí)行complete,并且?guī)线@個error。否則再執(zhí)行right,并將right的結(jié)果調(diào)用complete。然后在用+>
連接了一組異步操作的時(shí)候,一旦有錯,這個邏輯就可以讓錯誤一步步傳播到最頂層,避免執(zhí)行了冗余的代碼。
一個稍微異步一點(diǎn)的例子
隨便建一個single view application,在viewcontroller.swift
的頂部(swift 要求 operator 定義在 file scope,所以不能寫在類里),添加:
typealias AsyncFunc = (info : AnyObject,complete:(AnyObject?,NSError?)->Void) -> Void
infix operator +> {}
func +> (left:AsyncFunc,right:AsyncFunc) -> AsyncFunc{
return { info , complete in
left(info: info){ result,error in
guard error == nil else{
complete(nil,error)
return
}
right(info: info){result,error in
complete(result,error)
}
}
}
}
然后修改viewDidLoad為如下代碼:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let identity:AsyncFunc = {info,complete in complete(nil,nil)}
func dispatchSecond(afterSecond : Int, block:dispatch_block_t){
let time = dispatch_time(DISPATCH_TIME_NOW, Int64(afterSecond) * Int64(NSEC_PER_SEC))
dispatch_after(time, dispatch_get_main_queue(), block)
}
let async1: AsyncFunc = { info, complete in
dispatchSecond(2, block: {
print("oh, im first one")
complete(nil, nil)
})
}
let async2: AsyncFunc = { info, complete in
dispatchSecond(2, block: {
print("oh, im second one")
complete(nil, nil)
})
}
let async3: AsyncFunc = { info, complete in
dispatchSecond(2, block: {
print("shit, im third one")
complete(nil, nil)
})
}
let async4: AsyncFunc = { info, complete in
dispatchSecond(2, block: {
print("fuck, im fourth one")
complete(nil, nil)
})
}
let asyncDaddy = [async1,async2,async3,async4].reduce(identity, combine: +>)
asyncDaddy(info: 0) { (o, e) in
print("okay, im deadly a last one")
}
}
運(yùn)行程序后,會每兩秒有一個輸出。:)
本文旨在拋磚引玉,其實(shí)swift的functional特性已經(jīng)非常豐富,稍微探索一下是可以做出很多fancy的應(yīng)用出來的。
在函數(shù)式編程的世界里,我們定義的
identity
加上+>
就是一種monoid,常見的monoid還有:
加法: identity 就是 0 , +> 就對應(yīng) +
乘法:identity 就是 1 , +> 就對應(yīng) *
一點(diǎn)有趣的思考: 剛才我們已經(jīng)解釋了,我們的
+>
運(yùn)算符是不支持交換律的,因?yàn)槭谴小D撬欠裰С纸Y(jié)合律呢? 比如:(a +> b) +> c
是否等于a +> (b +> c)
?