第六章——函式(函式的便捷性)

bestswifter發表於2017-12-27

本文系閱讀閱讀原章節後總結概括得出。由於需要我進行一定的概括提煉,如有不當之處歡迎讀者斧正。如果你對內容有任何疑問,歡迎共同交流討論。

柯里化函式(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的強大和靈活之處。不過考慮一個稍複雜的問題:一個陣列中有多個字典,每個字典都有兩個鍵,lastNamefirstName,現在我們對陣列按照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這樣的語言一樣,擁有了很強大的動態特性。

相關文章