Swift函数式编程三(Map、Filter和Reduce)
代码地址
泛型介绍
需求为写一个这样的函数,此函数接收一个参数为整型数组,返回一个一个新数组,新数组各项为原数组对应的数据加一。
func incrementArray(array: [Int]) -> [Int] {
var result: Array<Int> = []
for i in array {
result.append(i + 1)
}
return result
}
新增需求,再写一个函数,此函数接收一个参数为整型数组,返回一个一个新数组,新数组各项为原数组对应的数据两倍。
func doubleArray(array: [Int]) -> [Int] {
var result: [Int] = []
for i in array {
result.append(i*2)
}
return result;
}
至此,发现这两个函数有大量的代码相同,能不能写一个更通用的函数?新增一个参数接收一个函数,这个参数根据各个数组项计算新的值。
func doubleArray(array: [Int]) -> [Int] {
var result: [Int] = []
for i in array {
result.append(i*2)
}
return result;
}
这样就可以简化一点incrementArray、doubleArray这两个函数:
func incrementArray1(array: [Int]) -> [Int] {
return computeIntArray(array: array, transform: { x in x + 1 })
}
func doubleArray1(array: [Int]) -> [Int] {
return computeIntArray(array: array, transform: { x in x*2 })
}
代码任然不够灵活,如果需要得到一个布尔型数组,用于表示对应的数字是否为偶数。
func isEvenArray(array: [Int] -> [Bool]) {
return computeIntArray(array: array, transform: { x in x%2 == 0 })
}
不幸的是上面这段代码无法使用computeIntArray函数,因为类型错误。
于是可以定义一个新的函数接受一个Int -> Bool类型的函数作为参数。
但是这个方案并不好,如果还要计算String类型,还得定义一个高阶函数来接受一个Int -> String类型的函数作为参数。
幸运的是泛型可以解决这个问题。相同的代码可以适用任何类型,写一个适用于每种可能类型的泛型函数:
func genericComputeArray<T>(array: [Int], transform: (Int) -> T) -> [T] {
var result: [T] = []
for i in array {
result.append(transform(i))
}
return result
}
可以进一步一般化这个函数,没有理由仅能对[Int]型的输入数组进行处理,可以将数组类型也进行抽象:
func map<Element, T>(array: [Element], transform: (Element) -> T) -> [T] {
var result: [T] = []
for i in array {
result.append(transform(i))
}
return result
}
对于这个map函数在两个维度是通用的,任何类型的数组和transform函数。
按照Swift的惯例将map函数定义为Array的扩展会比定义为顶层函数更合适:
extension Array {
func map<T>(transform: (Element) -> T) -> [T] {
var result: [T] = []
for i in self {
result.append(transform(i))
}
return result
}
}
Element源于Swift的Array中对Element所进行的泛型定义。
map函数已经是Swift标准库中的一部分(基于SequenceType协议被定义)
顶层函数和扩展
在一开始将创建map函数时,为了简便起见,选择了顶层函数的版本。不过最终将map的泛型版本定义为Array的扩展,这和Swift标准库的实现十分相似。
随着协议扩展(protocol extensions),开发者有了强有力的工具来定义扩展--不仅可以在Array这样的具体类型上上进行定义,还可以在SequenceType这样的协议上定义扩展。
把处理确定类型的函数,定义为该类型的扩展。这样的优点是:
- 自动补全更完善
- 命名更少
- 代码结构更清晰
Filter
filter函数像之前定义的map函数一样,接收一个函数作为参数,这个参数类型是(Elemeng) -> Bool--对于数组中的所有元素,此函数判定它是否被包含在结果中:
extension Array {
func filter(includeElement: (Element) -> BooleanLiteralType) -> [Element] {
var result: [Element] = []
for i in self {
if (includeElement(i)) { result.append(i) }
}
return result
}
}
Swift标准库中的数组类型已经定义好了filter函数。
有没有更通用的函数,可以用来定义map,也可以定义filter?
Reduce
reduce函数将变量初始化为某个值,然后对数组的每一项进行遍历,以某种方式更新结果。
extension Array {
func reduce<T>(initial: T, combine: (T, Element) -> T) -> T {
var result: T = initial
for i in self {
result = combine(result, i)
}
return result
}
}
reduce函数的泛型体现在两个方面:
- 对于任意类型的数组,它会计算一个T类型的返回值。
- 需要一个T类型的初始值,以及一个用于更新for循环中变量值的函数combine: (T, Elemeng) -> T。
使用reduce定义函数。除了使用闭包,也可以使用操作符作为最后一个参数,使代码更简短。
func sumUsingReduce(xs: [Int]) -> Int {
return xs.reduce(initial: 0, combine: { result, i in result + i })
}
func productUsingReduce(xs: [Int]) -> Int {
return xs.reduce(initial: 1, combine: *)
}
func concatUsingReduce(xs: [String]) -> String {
return xs.reduce(initial: "", combine: +)
}
甚至可以使用reduce重新定义map、filter。
extension Array {
func mapUsingReduce<T>(transform: (Element) -> T) -> [T] {
return self.reduce(initial: [T](), combine: { result, i in result + [transform(i)] })
}
func filterUsingReduce(includeElement: (Element) -> Bool) -> [Element] {
return self.reduce(initial: [Element](), combine: { result, i in includeElement(i) ? result + [i] : result })
}
}
能够用reduce表示这些函数,说明了reduce能够通过通用的方法来体现一种常见的编程模式:遍历数组并计算结果。
注意:
使用reduce来定义一切非常的简便,但是实践中这往往不是一个好主意。原因是,代码在最终的运行期间大量复制生成的数组,换句话说,它不得不反复的分配内存释放内存以及复制内存中的内容。像之前那样用一个可变数组定义map显然效率更高。理论上,编译器可以优化代码使其速度和可变数组一样快,但是Swift2.0并没有做优化。
实际运用
假设有一个City结构体,由城市名称和人口(万)组成。并定义了一些城市示例。
struct City {
let name: String
let population: Int
}
let beijing = City(name: "北京", population: 4000)
let shanghai = City(name: "上海", population: 3500)
let guangzhou = City(name: "广州", population: 3000)
let shenzhen = City(name: "深圳", population: 2500)
let citys = [beijing, shanghai, guangzhou, shenzhen]
现在赛选出居民数量至少为3000万的城市,并打印一份这些城市名称及人口数的列表。
extension City {
func cityByScalingPopulation() -> City { return City(name: self.name, population: self.population*10000) }
}
let table = citys.filter(includeElement: { city in city.population >= 3000 }).map(transform: { city in city.cityByScalingPopulation() }).reduce(initial: "\n城市:人口\n", combine: { result, city in result + "\n\(city.name):\(city.population)\n"})
泛型和Any类型
Any类型和泛型都能定义接收不同类型的参数的函数。然而两者之间的重要区别是:
- 泛型可以用于定义灵活的函数,类型检查仍由编译器负责。
- Any类型直接避开了Swift的类型系统(尽可能避免使用)。
用泛型和Any类型分别构造一个函数,除了返回它的参数,其他什么也不做。
func noOp<T>(x: T) -> T { return x }
func noOpAny(x: Any) -> Any { return x }
noOp和noOpAny函数都接收任意参数,关键区别在于返回值,noOp的返回值类型必须跟参数一样,而noOpAny的返回值可以为任何类型,甚至可以和参数的类型不同。如下函数noOpWrong会导致类型错误:
func noOpWrong<T>(x: T) -> T { return 0 }
func noOpAnyWrong(x: Any) -> Any { return 0 }
泛型函数的类型十分丰富,考虑把上一篇Swift函数式编程二(封装Core Image)中的函数组合运算符>>>定义为泛型版本:
precedencegroup ComposeFunctionPrecedence {
associativity: left
}
infix operator >>>: ComposeFunctionPrecedence
func >>><A, B, C>(f: @escaping (A) -> B, g: @escaping (B) -> C) -> (A) -> C {
return { x in g(f(x)) }
}
最后用相同的方式定义一个泛型函数,这个函数的作用是将接受两个参数作为输入的函数进行柯里化处理,生成相应的柯里化版本:
func curry<A, B, C>(f: @escaping (A, B) -> C) -> (A) -> (B) -> C {
return { x in return { y in f(x, y) } }
}
使用泛型,能够在不牺牲类型安全的情况下写出灵活的函数;而使用Any类型,则无法办到。