不學點高階函數,如何愉快的裝逼!
如果你開始接觸函數式編程,你一定聽說過高階函數。在維基百科它的中文解釋是這樣的:
在數學和計算機科學中,高階函數是至少滿足下列一個條件的函數:
- 接受一個或多個函數作為輸入
- 輸出一個函數
看起它就是ObjC語言中入參或者返回值為block的block或者函數,在Swift語言中即為入參或者返回值為函數的函數。那它們在實際的開發過程中究竟起著什么樣的作用呢?我們將從入參、返回值和綜合使用三部分來看這個問題:
函數作為入參
函數作為入參似乎無論在ObjC時代還是Swift時代都是司空見慣的事情,例如AFNetworking就用兩個入參block分別回調成功與失敗。Swift中更是加了一個尾閉包的語法(最后一個參數為函數,可以不寫括號或者寫到括號外面直接跟隨方法名),例如下面這樣:
- [1, 2, 3].forEach { item in
- print(item)
- }
我們可以將入參為函數的函數分為兩類,escaping函數入參和noescape函數入參,區別在于這個入參的函數是在執行過程內被調用還是在執行過程外被調用。執行過程外被調用的一般用于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的入參函數就作為網絡請求回來的一個callback,并不會在執行responseJSON這個函數的時候被調用。另外我們來觀察forEach的代碼,可以推斷入參的函數一定會在forEach執行過程中使用,執行完就沒有利用意義,這類就是noescape函數。
callback的用法大家應該比較熟悉了,介紹給大家noescape入參的一些用法:
1. 自由構造器
看過GoF設計模式的同學不知道是否還記得構造器模式,Android中的構造器模式類似如下:
- new AlertDialog.Builder(this)
- .setIcon(R.drawable.find_daycycle_icon)
- .setTitle("提醒")
- .create()
- .show();
- 構造一個對象需要很多的參數
- 這些參數里面很多有默認值
- 這些參數對應的屬性未來不希望被修改
那么用這樣的模式就可以直觀又精巧的展示構建過程。
如果使用noescape入參函數還可以更簡單的構造出這種代碼,只需要傳入一個入參為builder的對象就可以了,如下:
- // 實現在這里 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 入參的使用
- prop1 = someBuilder.prop1
- prop2 = someBuilder.prop2
- prop3 = someBuilder.prop3
- }
- }// 使用的時候 let someOjb = SomeObj { builder in
- builder.prop1 = 15
- builder.prop2 = false
- builder.prop3 = "haha"}
2. 自動配對操作
很多時候,我們開發過程中都會遇到必須配對才能正常工作的API,例如打開文件和關閉文件、進入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() // 容易漏掉或者上面出現異常 }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入參,我們可以將要操作的過程封裝起來,使得上層看起來更規整
- // 擴展一下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)
- }
- }
函數作為入參就簡單介紹到這里,下面看看函數作為返回值。
函數作為返回值
在大家的日常開發中,函數作為返回值的情況想必是少之又少。不過,如果能簡單利用起來,就會讓代碼一下子清爽很多。
首先沒有爭議的就是我們有很多的API都是需要函數作為入參的,無論是上一節提到過的escaping入參還是noescape入參。所以很多的時候,大家寫的代碼重復率會很高,例如:
- 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}
一段從數組中找到大于某個數平方的代碼,如果不封裝,看起來應該是這樣的。為了簡化,通常會封裝成如下的兩個樣子:
- 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)
如果用高階函數的返回值函數,可以做成這樣一個高階函數:
- // 一個返回(Int)->Bool的函數 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))
你一定會說,兩者看起來沒啥區別。所以這里面需要講一下使用高階返回函數的幾點好處
1. 不需要wrapper函數也不需要打開原始類
如同上面的簡單封裝,其實就是一個wrapper函數,把array作為入參帶入進來。這樣寫代碼和看代碼的時候就稍微不爽一點,畢竟大家都喜歡OOP嘛。如果要OOP,那就勢必要對原始類進行擴展,一種方式是加extension,或者直接給類加一個新的方法。
2. 閱讀代碼的時候一目了然
使用簡單封裝的時候,看代碼的人并不知道內部使用了filter這個函數,必須要查看源碼才能知道。但是用高階函數的時候,一下子就知道了使用了系統庫的filter。
3. 更容易復用
這也是最關鍵的一點,更細粒度的高階函數,可以更方便的復用,例如我們知道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))
回憶下上面的簡單封裝,是不是就無法重用了呢?
類似的返回函數的高階函數還可以有很多例子,例如上面說過的builder,假如每次都需要定制成特殊的樣子,但是某個字段不同,就可以用高階函數很容易打造出來:
- 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"))
介紹完入參與返回值,還有另外的一個組合模式,那就是入參是一個函數,返回值也是一個函數的情況,我們來看看這種情況。
入參函數 && 返回值函數
這樣的一個函數看起來會很恐怖,swift會聲明成:
- func someFunc<A, B, C, D>(_ a: (A) -> B)-> (C) -> D
objective-c會聲明成
- - (id (^)(id))someFunc:(id (^)(id))block
讓我們先從一個小的例子來講起,回憶一下我們剛剛做的biggerThanPow2With這個函數,如果我們要一個notBiggerThanPow2With怎么辦呢?你知道我一定不會說再寫一個。所以我告訴你我會這樣寫:
- func not<T>(_ origin_func: @escaping (T) -> Bool) -> (T) -> Bool { return { !origin_func($0) }
- }let array5 = array.filter(not(biggerThanPow2With(value: 9)))
并不需要一個notBiggerThanPow2With函數,我們只需要實現一個not就可以了。它的入參是一個(T) -> Bool,返回值也是(T) -> Bool,只需要在執行block內部的時候用個取反就可以了。這樣不單可以解決剛才的問題,還可以解決任何(T) -> Bool類型函數的取反問題,比如我們有一個odd(_: int)方法來過濾奇數,那我們就可以用even=not(odd)得到一個過濾偶數的函數了。
- 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不是一個返回函數的高階函數,那它就不太容易用not函數來加工了。
綜上,如果一個入參和返回值都是函數的函數就是這樣的一個轉換函數,它能夠讓我們用更少的代碼組合出更多的函數。另外需要注意一下,如果返回的函數里面閉包了入參的函數,那么入參函數就是escaping入參了。
下面再展示給大家兩個函數,一個交換參數的函數exchangeParam,另一個是柯里化函數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) }
- }
swift語言里面>是一個入參(a, b)的函數,所以>(5, 3) == true。我們使用exchangeParam交換參數就變成了(b, a),這時exchangeParam(>)(5, 3)就等于false了。
而currying函數又把參數b固定為一個常量9,所以currying(exchangeParam(>), 9)就是大于9的函數意思。
這個例子里就利用了全部的預制函數和通用函數,沒有借助任何的命令與業務函數聲明實現了一個從數組中過濾大于9的子數組的需求。試想一下,如果我們更多的使用這樣的高階函數,代碼中是不是很多的邏輯可以更容易的互相組合,而這就是函數式編程的魅力。
總結
高階函數的引入,無論是從函數式編程還是從非函數式編程都帶給我們代碼一定程度的簡化,使得我們的API更加簡易可用,復用更充分。然而本文的例子不過是冰山一角,更多的內容還需要大家的不斷嘗試和創新,也可以通過學習更多的函數式編程范式來加深理解。
作者介紹: