高阶函数---swift中的泛型介绍(一步步实现Map函数)

说明

本文内容均出自函数式 Swift一书, 此处整理仅仅是为了自己日后方便查看, 需要深入研究的话, 可以点进去购买, 支持原作者 本书由 王巍–新浪微博大神翻译 OneV’s Den 喵神博客


接受其它函数作为参数的函数有时被称为高阶函数. 本篇中, 将在一些来自Swift标准库中作用于数组的高阶函数中漫游. 伴随这个过程, 我们将介绍Swift的泛型, 以及展示如何将复杂计算运用于数组中.

泛型介绍

假如我们需要写一个函数, 它接受一个给定的整型数组, 通过计算得到并返回一个新数组, 新数组各项为原数组中对应的整型数据加一. 这一切, 仅仅只需要使用一个for循环就能非常容易地实现:

func incrementArray(xs: [Int]) -> [Int] {
        var result: [Int] = []
        for x in xs {
            result.append(x + 1)
        }
        return result
    }

现在假设我们还需要一个函数, 用于生成一个每项都为参数数组对应项两倍的数组. 这同样能很容易地使用一个for循环来实现:

func doubleArray(xs: [Int]) -> [Int] {
        var result: [Int] = []
        for x in xs {
            result.append(x * 2)
        }
        return result
    }

这两个函数有大量相同的代码, 我们能不能将没有区别的地方抽象出来, 并单独写一个体现这种模式且更通用的函数呢? 像这样的函数需要追加一个新参数来接受一个函数, 这个参数能根据各个数组项计算得到新的整型数值:

func computeIntArray(xs: [Int], transform: ((Int) -> Int) ) -> [Int] {
        var result: [Int] = []
        for x in xs {
            result.append(transform(x))
        }
        return result
    }

现在, 取决于我们想如何根据原数组得到一个新数组, 我们可以向函数传递不同的参数. doubleArray函数incrementArray函数 都精简为了一行调用 computeInArray的语句:

func doubleArrany2(xs: [Int]) -> [Int] {
        return computeIntArray(xs: xs, transform: { (x) -> Int in
            x * 2
        })
    }

代码仍然不像想象中的那么灵活. 假设我们想要得到一个布尔型的新数组, 用于表示原数组中对应的数字是否是偶数. 我们可能会尝试编写一些像下面这样的代码:

func isEvenArray(xs: [Int]) -> [Bool] {
        computeIntArray(xs: xs) { (x) -> Int in
            x % 2 == 0
        }
    }

不幸的是, 这段代码导致了一个类型错误. 问题在于, 我们的computeIntArray函数接受一个 Int -> Int 类型的参数, 也就是说, 该参数是一个返回整型值的函数. 而在isEvenArray函数的定义中, 我们传递了一个Int -> Bool类型的参数, 于是导致了类型错误.

类型错误

我们应该如何解决这个问题呢? 一种可行的方案是定义新版本的computeIntArray函数, 接受一个Int -> Bool 类型的函数作为参数. 类似下面这样:

func computeBoolArray(xs: [Int], transform2: (Int) -> Bool) -> [Bool] {
        var result: [Bool] = []
        for x in xs {
            result.append(transform2(x))
        }
        return result
    }

但是, 这个方案的扩展性并不好. 如果接下来需要计算String类型呢? 我们是否还需要定义一个高阶函数来接受 Int -> String类型的参数?

幸运的是, 该问题有一个解决方案: 我们可以使用_泛型_. computeBoolArraycomputeIntArray 的定义是相同的; 唯一的区别在于类型签名(type signature). 假如我们定义一个相似的函数computeStringArray 来支持String类型, 其函数体将会与先前两个函数完全一致. 事实上, 相同部分的代码可以用于任何类型. 我们正真想做的是写一个能够使用于每种可能类型的泛型函数:

func genericComputeArray1<T>(xs: [Int], transform: (Int) -> T) -> [T] {
        var result: [T] = []
        for x in xs {
            result.append(transform(x))
        }
        return result
    }

关于这段代码, 最有意思的是它的类型签名. 理解这个类型签名有助于你将genericComputeArray1<T>理解为一个函数族. 类型参数T的每个选择都会确定一个新函数. 该函数接受一个整型数组和一个Int -> T类型的函数作为参数, 并返回一个[T]类型的数组.

我们仍能进一步将这个函数一般化. 没有理由让它仅能对类型为[Int]的输入数组进行处理. 将数组类型进行抽象, 能得到下面这样的类型签名:

func map<Element, T>(xs: [Element], transform: (Element) -> T) -> [T] {
        var result: [T] = []
        for element in xs {
            result.append(transform(element))
        }
        return result
    }

这里我们写了一个 map函数, 它在两个维度都是通用的: 对于任何Element的数组和 transform: Element -> T 函数, 它都会生成一个T的新数组. 这个map函数甚至比我们之前看到的genericComputeArray函数更通用. 事实上, 我们可以通过map来定义genericComputeArray:

func genericComputeArray<T>(xs: [Int], transform: (Int) -> T) -> [T] {
        return map(xs: xs, transform: transform)
    }

同样的, 上述函数的定义并没有什么太过特别之处: 函数接受xstransform两个参数之后, 将他们传递给map函数, 然后返回结果. 关于这个定义, 最有意思非类型莫属. genericComputeArray<T>(xs: [Int], transform: (Int) -> T) -> [T] map函数的一个实例, 只是它有一个更具体的类型.

实际上, 比起定义一个顶层map函数, 按照Swift的惯例将map定义为Array的扩展更合适:

extension Array {
    func map<T>(transform: (Element) -> T) -> [T] {
        var result: [T] = []
        for x in self {
            result.append(transform(x))
        }
        return result
    }
}

我们子啊函数的transform参数中所使用的Element类型源自于SwiftArray中对Element所进行的泛型定义.

作为map(xs, transform)的替代, 我们现在可以通过xs.map(transform)来调用Arraymap函数:

func genericComputeArray<T>(xs: [Int], transform: (Int) -> T) -> [T] {
        return xs.map(transform: transform)
    }

其实我们并不需要自己定义像这样的map函数, 因为它已经是Swift标准库的一部分了(基于SequenceType协议被定义)

顶层函数 和 类型扩展

你可能已经注意到, 在本节的函数中我们使用了两种不同的方式来声明函数: 顶层函数和类型扩展. 在一开始创建map函数的过程中, 为了简单起见, 我们选择了顶层函数的版本作为例子进行展示. 不过, 最终我们将map的泛型版本定义为Array的扩展, 这与它在Swift标准库中的实现方式十分相似.

Swift标准库最初的版本中, 顶层函数仍然是无处不在的, 但伴随Swift2.0的诞生, 这种模式被彻底从标准库中移除了. 随着协议扩展(protocol extension), 当前第三方开发者有了一个强有力的工具来定义他们自己的扩展–现在我们不仅仅可以再Array这样的具体类型上进行定义, 还可以在Sequence Type一样的协议上来定义扩展.


我们建议遵循此规则, 并把处理确定类型的函数定义为该类型的扩展 . 这样做的优点是自动补齐更完善, 暧昧的命名更少, 以及(通常)代码结构更清晰.

下一篇

高阶函数—swift中的Filter 和 Reduce

Loading Disqus comments...
Table of Contents