把玩高階函數(shù)(轉(zhuǎn))

http://mp.weixin.qq.com/mp/getmasssendmsg?__biz=MjM5NTIyNTUyMQ==#wechat_webview_type=1&wechat_redirect
推薦序:本文是來自美團的 iOS 技術(shù)專家臧成威的投稿查描。主要介紹的是在我們的日常編程中软吐,引入高階函數(shù)所帶來的便利。順便說一下,臧老師在 StuQ 開完 RactiveCocoa 的兩次系列課程后入录,最近新開了一門 《iOS 實戰(zhàn)黑魔法》的新課程渊额,課程內(nèi)容涉及很多 Objective-C Runtime, Swift 等底層的知識和應用技巧巡蘸,如果你感興趣它褪,可以看文末的介紹。

感謝臧成威的授權(quán),以下是文章正文啤贩。

如果你開始接觸函數(shù)式編程待秃,你一定聽說過高階函數(shù)。在維基百科它的中文解釋是這樣的:

在數(shù)學和計算機科學中痹屹,高階函數(shù)是至少滿足下列一個條件的函數(shù):

  • 接受一個或多個函數(shù)作為輸入
  • 輸出一個函數(shù)

看起它就是 ObjC 語言中入?yún)⒒蛘叻祷刂禐?block 的 block 或者函數(shù)章郁,在 Swift 語言中即為入?yún)⒒蛘叻祷刂禐楹瘮?shù)的函數(shù)。那它們在實際的開發(fā)過程中究竟起著什么樣的作用呢志衍?我們將從入?yún)⑴⒎祷刂岛途C合使用三部分來看這個問題:

函數(shù)作為入?yún)?/h2>

函數(shù)作為入?yún)⑺坪鯚o論在 ObjC 時代還是 Swift 時代都是司空見慣的事情,例如AFNetworking就用兩個入?yún)?block 分別回調(diào)成功與失敗楼肪。Swift 中更是加了一個尾閉包的語法(最后一個參數(shù)為函數(shù)培廓,可以不寫括號或者寫到括號外面直接跟隨方法名),例如下面這樣:

[1, 2, 3].forEach { item in  
    print(item)  
}

我們可以將入?yún)楹瘮?shù)的函數(shù)分為兩類春叫,escaping 函數(shù)入?yún)⒑?noescape 函數(shù)入?yún)⒓缒疲瑓^(qū)別在于這個入?yún)⒌暮瘮?shù)是在執(zhí)行過程內(nèi)被調(diào)用還是在執(zhí)行過程外被調(diào)用。執(zhí)行過程外被調(diào)用的一般用于 callback 用途暂殖,例如:

Alamofire.request("https://httpbin.org/get").responseJSON { 
        response in  
    print(response.request)  // original URL request  
    print(response.response) // HTTP URL response  
    print(response.data)     // server data  
    print(response.result)   // result of response serialization  

    if let JSON = response.result.value {  
        print("JSON: \(JSON)")  
    }  
}

這個 response 的入?yún)⒑瘮?shù)就作為網(wǎng)絡(luò)請求回來的一個 callback价匠,并不會在執(zhí)行 responseJSON 這個函數(shù)的時候被調(diào)用。另外我們來觀察forEach的代碼呛每,可以推斷入?yún)⒌暮瘮?shù)一定會在forEach執(zhí)行過程中使用霞怀,執(zhí)行完就沒有利用意義,這類就是 noescape 函數(shù)莉给。

callback 的用法大家應該比較熟悉了,介紹給大家 noescape 入?yún)⒌囊恍┯梅ǎ?/p>

1. 自由構(gòu)造器

看過 GoF 設(shè)計模式的同學不知道是否還記得構(gòu)造器模式廉沮,Android 中的構(gòu)造器模式類似如下:

new AlertDialog.Builder(this)  
  .setIcon(R.drawable.find_daycycle_icon)  
  .setTitle(" 提醒 ")  
  .create()  
  .show();

如果你想要做成這樣的代碼颓遏,你需要將setIconsetTitle滞时、create等方法都實現(xiàn)成返回this才行叁幢。這樣就無法直接利用無返回值的setter了。
為什么需要這樣的方式呢坪稽?如果你同時有如下需求:

構(gòu)造一個對象需要很多的參數(shù)
這些參數(shù)里面很多有默認值
這些參數(shù)對應的屬性未來不希望被修改
那么用這樣的模式就可以直觀又精巧的展示構(gòu)建過程曼玩。

如果使用 noescape 入?yún)⒑瘮?shù)還可以更簡單的構(gòu)造出這種代碼,只需要傳入一個入?yún)?builder 的對象就可以了窒百,如下:

// 實現(xiàn)在這里  class SomeBuilder {  
    var prop1: Int  
    var prop2: Bool  
    var prop3: String  
    init() {  
        // default value  
        prop1 = 0  
        prop2 = true  
        prop3 = "some string"  
    }  
}  

class SomeObj {  
    private var prop1: Int  
    private var prop2: Bool  
    private var prop3: String  
    init(_ builderBlock:(SomeBuilder) -> Void) {  
        let someBuilder = SomeBuilder()  
        builderBlock(someBuilder) // noescape 入?yún)⒌氖褂? 
        prop1 = someBuilder.prop1  
        prop2 = someBuilder.prop2  
        prop3 = someBuilder.prop3  
    }  
}  

// 使用的時候  let someOjb = SomeObj { builder in  
    builder.prop1 = 15  
    builder.prop2 = false  
    builder.prop3 = "haha"  }

2.自動配對操作

很多時候黍判,我們開發(fā)過程中都會遇到必須配對才能正常工作的 API,例如打開文件和關(guān)閉文件篙梢、進入 edit 模式退出 edit 模式等顷帖。雖然 swift 語言給我們 defer 這樣的語法糖避免大家忘記配對操作,但是代碼看起來還是不那么順眼

func updateTableView1() {  
    self.tableView.beginUpdates()  

    self.tableView.insertRows(at: [IndexPath(row: 2, section: 0)], with: .fade)  
    self.tableView.deleteRows(at: [IndexPath(row: 5, section: 0)], with: .fade)  

    self.tableView.endUpdates() // 容易漏掉或者上面出現(xiàn)異常  }  

func updateTableView2() {  
    self.tableView.beginUpdates()  
    defer {  
        self.tableView.endUpdates()  
    }  

    self.tableView.insertRows(at: [IndexPath(row: 2, section: 0)], with: .fade)  
    self.tableView.deleteRows(at: [IndexPath(row: 5, section: 0)], with: .fade)  
}

利用 noescape 入?yún)ⅲ覀兛梢詫⒁僮鞯倪^程封裝起來贬墩,使得上層看起來更規(guī)整

// 擴展一下 UITableView  extension UITableView {  
    func updateCells(updateBlock: (UITableView) -> Void) {  
        beginUpdates()  
        defer {  
            endUpdates()  
        }  
        updateBlock(self)  
    }  
}  

func updateTableView() {  
    // 使用的時候  
    self.tableView.updateCells { (tableView) in  
        tableView.insertRows(at: [IndexPath(row: 2, section: 0)], with: .fade)  
        tableView.deleteRows(at: [IndexPath(row: 5, section: 0)], with: .fade)  
    }  
}

函數(shù)作為入?yún)⒕秃唵谓榻B到這里榴嗅,下面看看函數(shù)作為返回值。

函數(shù)作為返回值

在大家的日常開發(fā)中陶舞,函數(shù)作為返回值的情況想必是少之又少嗽测。不過,如果能簡單利用起來肿孵,就會讓代碼一下子清爽很多唠粥。

首先沒有爭議的就是我們有很多的 API 都是需要函數(shù)作為入?yún)⒌模瑹o論是上一節(jié)提到過的 escaping 入?yún)⑦€是 noescape 入?yún)渚K院芏嗟臅r候厅贪,大家寫的代碼重復率會很高,例如:

let array = [1, 3, 55, 47, 92, 77, 801]  

let array1 = array.filter { $0 > 3 * 3}  
let array2 = array.filter { $0 > 4 * 4}  
let array3 = array.filter { $0 > 2 * 2}  
let array4 = array.filter { $0 > 5 * 5}

一段從數(shù)組中找到大于某個數(shù)平方的代碼雅宾,如果不封裝养涮,看起來應該是這樣的。為了簡化眉抬,通常會封裝成如下的兩個樣子:

func biggerThanPowWith(array: [Int], value: Int) -> [Int] {  
    return array.filter { $0 > value * value}  
}  

let array1 = biggerThanPowWith(array: array, value: 3)  
let array2 = biggerThanPowWith(array: array, value: 4)  
let array3 = biggerThanPowWith(array: array, value: 2)  
let array4 = biggerThanPowWith(array: array, value: 5)

如果用高階函數(shù)的返回值函數(shù)贯吓,可以做成這樣一個高階函數(shù):

// 一個返回 (Int)->Bool 的函數(shù)  func biggerThanPow2With(value: Int) -> (Int) -> Bool {  
    return { $0 > value * value }  
}  

let array1 = array.filter(biggerThanPow2With(value: 3))  
let array2 = array.filter(biggerThanPow2With(value: 4))  
let array3 = array.filter(biggerThanPow2With(value: 2))  
let array4 = array.filter(biggerThanPow2With(value: 5))

你一定會說,兩者看起來沒啥區(qū)別蜀变。所以這里面需要講一下使用高階返回函數(shù)的幾點好處

1. 不需要 wrapper 函數(shù)也不需要打開原始類

如同上面的簡單封裝悄谐,其實就是一個 wrapper 函數(shù),把array作為入?yún)脒M來库北。這樣寫代碼和看代碼的時候就稍微不爽一點爬舰,畢竟大家都喜歡 OOP 嘛。如果要 OOP寒瓦,那就勢必要對原始類進行擴展情屹,一種方式是加 extension,或者直接給類加一個新的方法杂腰。

2. 閱讀代碼的時候一目了然

使用簡單封裝的時候垃你,看代碼的人并不知道內(nèi)部使用了filter這個函數(shù),必須要查看源碼才能知道喂很。但是用高階函數(shù)的時候惜颇,一下子就知道了使用了系統(tǒng)庫的filter。

3. 更容易復用

這也是最關(guān)鍵的一點少辣,更細粒度的高階函數(shù)凌摄,可以更方便的復用,例如我們知道 Set也是有filter這個方法的漓帅,復用起來就這樣:

let set = Set<Int>(arrayLiteral: 1, 3, 7, 9, 17, 55, 47, 92, 77, 801)  
let set1 = set.filter(biggerThanPow2With(value: 3))  
let set2 = set.filter(biggerThanPow2With(value: 9))

回憶下上面的簡單封裝望伦,是不是就無法重用了呢林说?

類似的返回函數(shù)的高階函數(shù)還可以有很多例子,例如上面說過的 builder屯伞,假如每次都需要定制成特殊的樣子锯梁,但是某個字段不同第岖,就可以用高階函數(shù)很容易打造出來:

func builerWithDifferentProp3(prop3: String) -> (SomeBuilder) -> Void {  
    return { builder in  
        builder.prop1 = 15  
        builder.prop2 = true  
        builder.prop3 = prop3  
    }  
}  

let someObj1 = SomeObj.init(builerWithDifferentProp3(prop3: "a"))  
let someObj2 = SomeObj.init(builerWithDifferentProp3(prop3: "b"))  
let someObj3 = SomeObj.init(builerWithDifferentProp3(prop3: "c"))

介紹完入?yún)⑴c返回值吗伤,還有另外的一個組合模式茁裙,那就是入?yún)⑹且粋€函數(shù),返回值也是一個函數(shù)的情況末融,我們來看看這種情況钧惧。

入?yún)⒑瘮?shù) && 返回值函數(shù)

這樣的一個函數(shù)看起來會很恐怖,swift 會聲明成func someFunc<A, B, C, D>(_ a: (A) -> B)-> (C) -> D勾习,objective-c 會聲明成- (id (^)(id))someFunc:(id (^)(id))block浓瞪。讓我們先從一個小的例子來講起,回憶一下我們剛剛做的biggerThanPow2With這個函數(shù)巧婶,如果我們要一個notBiggerThanPow2With怎么辦呢乾颁?你知道我一定不會說再寫一個。所以我告訴你我會這樣寫:

func not<T>(_ origin_func: @escaping (T) -> Bool) -> (T) -> Bool {  
    return { !origin_func($0) }  
}  

let array5 = array.filter(not(biggerThanPow2With(value: 9)))

并不需要一個notBiggerThanPow2With函數(shù)艺栈,我們只需要實現(xiàn)一個not就可以了英岭。它的入?yún)⑹且粋€(T) -> Bool,返回值也是(T) -> Bool湿右,只需要在執(zhí)行 block 內(nèi)部的時候用個取反就可以了诅妹。這樣不單可以解決剛才的問題,還可以解決任何(T) -> Bool類型函數(shù)的取反問題毅人,比如我們有一個odd(_: int)方法來過濾奇數(shù)吭狡,那我們就可以用even=not(odd)得到一個過濾偶數(shù)的函數(shù)了。

func odd(_ value: Int) -> Bool {  
    return value % 2 == 1  }  

let array6 = array.filter(odd)  
let array7 = array.filter(not(odd))  

let even = not(odd)  
let array8 = array.filter(even)

大家可以看下上面的biggerThanPow2With時我們討論過的丈莺,如果biggerThanPow2With不是一個返回函數(shù)的高階函數(shù)划煮,那它就不太容易用not函數(shù)來加工了。

綜上场刑,如果一個入?yún)⒑头祷刂刀际呛瘮?shù)的函數(shù)就是這樣的一個轉(zhuǎn)換函數(shù),它能夠讓我們用更少的代碼組合出更多的函數(shù)蚪战。另外需要注意一下牵现,如果返回的函數(shù)里面閉包了入?yún)⒌暮瘮?shù),那么入?yún)⒑瘮?shù)就是 escaping入?yún)⒘恕?/p>

下面再展示給大家兩個函數(shù)邀桑,一個交換參數(shù)的函數(shù)exchangeParam瞎疼,另一個是柯里化函數(shù)currying

func exchangeParam<A, B, C>(_ block: @escaping (A, B) -> C) -> (B, A) -> C {  
    return { block($1, $0) }  
}  

func currying<A, B, C>(_ block: @escaping (A, B) -> C, _ value: A) -> (B) -> C {  
    return { block(value, $0) }  
}

第一個函數(shù)exchangeParam是交換一個函數(shù)的兩個參數(shù),第二個函數(shù)currying是給一個帶兩個參數(shù)的函數(shù)和一個參數(shù)壁畸,返回一個帶一個參數(shù)的函數(shù)贼急。那這兩個函數(shù)究竟有什么用途呢茅茂?看一下下面的例子:

let array9 = array.filter(currying(exchangeParam(>), 9))

swift 語言里面>是一個入?yún)?(a, b) 的函數(shù),所以>(5, 3) == true太抓。我們使用exchangeParam交換參數(shù)就變成了 (b, a)空闲,這時exchangeParam(>)(5, 3)就等于false了。

而 currying 函數(shù)又把參數(shù)b固定為一個常量 9走敌,所以currying(exchangeParam(>), 9)就是大于 9 的函數(shù)意思碴倾。

這個例子里就利用了全部的預制函數(shù)和通用函數(shù),沒有借助任何的命令與業(yè)務(wù)函數(shù)聲明實現(xiàn)了一個從數(shù)組中過濾大于 9 的子數(shù)組的需求掉丽。試想一下跌榔,如果我們更多的使用這樣的高階函數(shù),代碼中是不是很多的邏輯可以更容易的互相組合捶障,而這就是函數(shù)式編程的魅力僧须。

總結(jié)

高階函數(shù)的引入,無論是從函數(shù)式編程還是從非函數(shù)式編程都帶給我們代碼一定程度的簡化项炼,使得我們的 API 更加簡易可用担平,復用更充分。然而本文的例子不過是冰山一角芥挣,更多的內(nèi)容還需要大家的不斷嘗試和創(chuàng)新驱闷,也可以通過學習更多的函數(shù)式編程范式來加深理解。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末空免,一起剝皮案震驚了整個濱河市空另,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蹋砚,老刑警劉巖扼菠,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異坝咐,居然都是意外死亡循榆,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門墨坚,熙熙樓的掌柜王于貴愁眉苦臉地迎上來秧饮,“玉大人,你說我怎么就攤上這事泽篮〉潦” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵帽撑,是天一觀的道長泼各。 經(jīng)常有香客問我,道長亏拉,這世上最難降的妖魔是什么扣蜻? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任逆巍,我火速辦了婚禮,結(jié)果婚禮上莽使,老公的妹妹穿的比我還像新娘锐极。我一直安慰自己,他們只是感情好吮旅,可當我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布溪烤。 她就那樣靜靜地躺著,像睡著了一般庇勃。 火紅的嫁衣襯著肌膚如雪檬嘀。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天责嚷,我揣著相機與錄音鸳兽,去河邊找鬼。 笑死罕拂,一個胖子當著我的面吹牛揍异,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播爆班,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼衷掷,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了柿菩?” 一聲冷哼從身側(cè)響起戚嗅,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎枢舶,沒想到半個月后懦胞,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡凉泄,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年躏尉,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片后众。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡胀糜,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蒂誉,到底是詐尸還是另有隱情教藻,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布拗盒,位于F島的核電站怖竭,受9級特大地震影響锥债,放射性物質(zhì)發(fā)生泄漏陡蝇。R本人自食惡果不足惜痊臭,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望登夫。 院中可真熱鬧广匙,春花似錦、人聲如沸恼策。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽涣楷。三九已至分唾,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間狮斗,已是汗流浹背绽乔。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留碳褒,地道東北人折砸。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像沙峻,于是被迫代替她去往敵國和親睦授。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,916評論 2 344

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