本文系閱讀閱讀原章節後總結概括得出。由於需要我進行一定的概括提煉,如有不當之處歡迎讀者斧正。如果你對內容有任何疑問,歡迎共同交流討論。
柯里化函式(Curried Function)
函式的柯里化通常被用於建立一組函式,並作為引數傳入到更高階的函式中。這個概念不太好理解,舉個實際例子來說明問題:假設我們需要判斷一個整數i
是不是另一個整數n
的整數倍。雖然n
是一個變數,它的值不確定,但是判斷邏輯總是相同的:i % n == 0
。所以判斷函式可以這樣寫:
func isMultipleOf(n n: Int, i: Int) -> Bool {
return i % n == 0
}
isMultipleOf(n: 2, i: 3) //false,3不是2的整數倍
isMultipleOf(n: 2, i: 4) //true,4是2的整數倍
複製程式碼
如果把isMultipleOf
作為引數傳入到高階函式,比如filter
中,程式碼會這樣寫:
let nums = 1...10
let evens = nums.filter { isMultipleOf(n: 2, i: $0) } // evens = [2,4,6,8,10]
複製程式碼
這種寫法的可讀性並不高,更好的解決方案是我們建立一個作為過渡的函式isEven
:
isEven = { isMultipleOf(n: 2, i: $0) }
let evens = nums.filter(isEven)
複製程式碼
這種寫法稍稍改進了可讀性,不過最好的方法還是定義一個柯里化函式:
func isMultipleOf(n n: Int)(_ i: Int) -> Bool {
return i % n == 0
}
複製程式碼
這個函式首先接收一個引數n
,然後返回一個函式。被返回的函式的型別是Int -> Bool
,它會判斷這個引數是否是n
的整數倍。所以isEven
函式可以這樣定義:
let isEven = isMultipleOf(n: 2)
複製程式碼
也可以省略這一步,直接寫filter
方法:
let evens = nums.filter(isMultipleOf(n: 2)) // evens = [2,4,6,8,10]
複製程式碼
最直觀的來看,相比於定義一個普通的函式,柯里化的isMultipleOf
在被傳入filter
函式中時,省略了第二個引數i
。回顧一下柯里化函式的定義就可以理解了:isMultipleOf(n: 2)
其實是原柯里化函式的返回值,這個值本身也是一個函式。
這裡我們用的是柯里化函式的簡單宣告方法,它把多個引數分別寫在多個括號中。柯里化函式還有一種完整的宣告方法:
func isMultipleOf(n n: Int) -> Int -> Bool {
return { i in
i % n == 0
}
}
複製程式碼
這裡我們顯式的宣告瞭isMultipleOf
方法的返回值型別。這兩種宣告方式是完全等價的。
排序問題
我們暫且把柯里化函式放在一邊,待會兒還有他大顯身手的機會。現在來看一個很簡單的陣列排序問題。我們知道陣列實現了sort
方法,預設是從小到大排序,如果想要指定排序規則,需要向sort
方法中傳入一個排序函式作為引數。這也正是Swift的強大和靈活之處。不過考慮一個稍複雜的問題:一個陣列中有多個字典,每個字典都有兩個鍵,lastName
和firstName
,現在我們對陣列按照lastName
的值進行排序,如果值相同就按照firstName
的值進行排序:
let last = "lastName", first = "firstName"
let people = [
[first: "Jo", last: "Smith"],
[first: "Joe", last: "Smith"],
[first: "Joe", last: "Smyth"],
[first: "Joanne", last: "Smith"],
[first: "Robert", last: "Jones"],
]
複製程式碼
如果我們使用OC中NSArray的sortedArrayUsingDescriptors
方法,問題就比較容易解決:
let lastDescriptor = NSSortDescriptor(key: last, ascending: true, selector: "localizedCaseInsensitiveCompare")
let firstDescriptor = NSSortDescriptor(key: first, ascending: true, selector: "localizedCaseInsensitiveCompare")
let descriptors = [lastDescriptor, firstDescriptor]
let sortedArray = (people as NSArray).sortedArrayUsingDescriptors(descriptors)
複製程式碼
這種做法的一大優勢在於descriptors
是排序函式的集合,它可以在執行時動態的建立。那麼怎麼用純Swift程式碼解決相同問題呢。首先來看一下只根據lastName
排序的解決方案:
let sortedArray = people.sort {
$0[last] < $1[last]
}
複製程式碼
但是如果一旦使用localizedCaseInsensitiveCompare
,這種寫法很快就變得非常醜。因為陣列的下標指令碼返回值是可選型別,無法直接使用localizedCaseInsensitiveCompare
方法:
let sortedArray = people.sort { lhs, rhs in
return rhs[first].flatMap {
lhs[first]?.localizedCaseInsensitiveCompare($0)
} == .OrderedAscending
}
複製程式碼
為了能在lastName
相同時比較firstName
,我們可以使用標準庫的lexicographicalCompare
方法。這個方法逐一比較兩個序列中的元素,直到比較出大小為止:
let sortedArray = people.sort { p0, p1 in
let left = [p0[last], p0[first]]
let right = [p1[last], p1[first]]
return left.lexicographicalCompare(right) {
guard let l = $0 else { return false }
guard let r = $1 else { return true }
return l.localizedCaseInsensitiveCompare(r) == .OrderedAscending
}
}
複製程式碼
雖然這樣可以實現排序功能,但依然有一些可以優化的地方。首先,在每一次排序時都新建陣列是很低效的。其次,比較方法是寫死的,不能動態的修改,而且對可選型別的處理導致程式碼不是很簡潔。我們首先優化一下可選型別的比較,這裡就用到了我們之前講的柯里化函式:
extension Optional {
func compare(rhs: Wrapped?, _ comparator: Wrapped -> Wrapped -> NSComparisonResult) -> Bool {
switch (self, rhs) {
case (nil, nil), (_?, nil): return false
case (nil, _?): return true
case let (l?, r?): return comparator(l)(r) == .OrderedAscending
}
}
}
複製程式碼
我們模仿可選型別的==
運算子(詳見可選型別技術之旅),實現了可選型別的compare
方法。其中的引數comparator
就是一個柯里化函式。於是,原來的sort
方法可以簡化成這樣:
let sortedArray = people.sort { p0, p1 in
let left = [p0[last], p0[first]]
let right = [p1[last], p1[first]]
return left.lexicographicalCompare(right) {
return $0.compare($1, String.localizedCaseInsensitiveCompare)
}
}
複製程式碼
函式作為資料
現在的sort
比之前簡潔了很多,不過比較的邏輯依然是hard-code的,我們需要模仿OC的sortedArrayUsingDescriptors
方法。其實我們用的lexicographicalCompare
是有問題的,它原本用於通過一個比較方法,依次比較兩組的元素,直到比較出順序為止。而我們現在的實際情況恰好完全相反:我們需要多個比較方法,比較固定的兩個元素,直到比較出順序為止,所以我們需要實現自己的lexicographicalCompare
方法:
func lexicographicalCompare<T>(comparators: [(T,T) -> Bool])(lhs: T, _ rhs: T) -> Bool {
for isOrderedBefore in comparators {
if isOrderedBefore(lhs, rhs) { return true }
if isOrderedBefore(rhs, lhs) { return false }
}
return false
}
複製程式碼
我們用每一個比較方法去比較這兩個元素,如果能比較出順序則返回true
,否則就互換元素位置。如果兩次都無法比較則說明這兩個元素是相等的(這需要保證每一個比較方法都是嚴格弱排序的),那麼就使用下一個比較方法。如果所有比較方法都無法比較出順序,則返回false
。
我們自定義的lexicographicalCompare
方法也是柯里化的,第一個引數是比較方法的陣列,接下來是待比較的兩個引數。於是sort
函式可以=被簡化成一行程式碼:
// 首先定義比較方法的陣列,先按照lastName排序,再按照firstName排序
let comparators: [([String: String], [String: String]) -> Bool] = [
{ $0[last].compare($1[last], String.localizedCaseInsensitiveCompare)},
{ $0[first].compare($1[first], String.localizedCaseInsensitiveCompare)},
]
let sortedArray = people.sort(lexicographicalCompare(comparators))
複製程式碼
這種方法幾乎與使用OC的sortedArrayUsingDescriptors
方法一樣簡單。這種方法不再把比較的邏輯寫死,因此具有很高的靈活性,比如如果要升序排列lastName
,但是降序排列firstName
,程式碼可以這樣寫:
let sortedArray = people.sort(lexicographicalCompare([
{ $0[last] < $1[last] },
{ $0[first] > $1[first] },
]))
複製程式碼
通過把函式作為資料,Swift這種靜態的、面向編譯的語言,也像OC、Ruby這樣的語言一樣,擁有了很強大的動態特性。