本文系阅读阅读原章节后总结概括得出。由于需要我进行一定的概括提炼,如有不当之处欢迎读者斧正。如果你对内容有任何疑问,欢迎共同交流讨论。
柯里化函数(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...10let 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 ini % 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 inreturn rhs[first].flatMap {lhs[first]?.localizedCaseInsensitiveCompare($0)} == .OrderedAscending}复制代码
为了能在lastName
相同时比较firstName
,我们可以使用标准库的lexicographicalCompare
方法。这个方法逐一比较两个序列中的元素,直到比较出大小为止:
let sortedArray = people.sort { p0, p1 inlet 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 falsecase (nil, _?): return truecase let (l?, r?): return comparator(l)(r) == .OrderedAscending}}}复制代码
我们模仿可选类型的==
运算符(详见),实现了可选类型的compare
方法。其中的参数comparator
就是一个柯里化函数。于是,原来的sort
方法可以简化成这样:
let sortedArray = people.sort { p0, p1 inlet 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(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这样的语言一样,拥有了很强大的动态特性。