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

345 阅读3分钟

本文系阅读阅读原章节后总结概括得出。由于需要我进行一定的概括提炼,如有不当之处欢迎读者斧正。如果你对内容有任何疑问,欢迎共同交流讨论。

柯里化函数(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这样的语言一样,拥有了很强大的动态特性。