【函数式 Swift】Map、Filter 和 Reduce

说明:本文及所属系列文章为图书《函数式 Swift》的读书笔记,旨在记录学习过程、分享学习心得。文中部分代码摘自原书开源代码库 Github: objcio/functional-swift,部份内容摘自原书。如需深刻学习,请购买正版支持原书。(受 @SwiftLanguage 微博启发,特此说明)javascript


标题中的三个数组操做函数咱们并不陌生,本章将借助这些 Swift 标准库中的函数,再次探索函数式思想的应用。html

本章关键词

请带着如下关键词阅读本文:java

  • 函数式思想
  • 泛型

案例:City Filter

使用 City 结构体描述城市信息(名字、人口数量),并定义一个城市数组:git

struct City {
    let name: String
    let population: Int
}

let paris = City(name: "Paris", population: 2241) // 单位为“千”
let madrid = City(name: "Madrid", population: 3165)
let amsterdam = City(name: "Amsterdam", population: 827)
let berlin = City(name: "Berlin", population: 3562)

let cities = [paris, madrid, amsterdam, berlin]复制代码

问题:输出 cities 数组中全部人口超过百万的城市信息,并将人口数量单位转换为“个”。github

开始解决问题以前,请你们先忘掉标题中 Map、Filter 和 Reduce 等函数,不管以前是否使用过,咱们尝试从零开始逐步向函数式思想过渡。express

咱们先使用一个简单的思路来解决这个问题,即,遍历输入的城市数组,而后依次判断每一个城市的人口数量,超过一百万的城市输出其信息:swift

func findCityMoreThanOneMillion(_ cities: [City]) -> String {
    var result = "City: Population\n"
    for city in cities {
        if city.population > 1000 {
            result = result + "\(city.name): \(city.population * 1000)\n"
        }
    }
    return result
}

let result = findCityMoreThanOneMillion(cities)
print(result)
// City: Population
// Paris: 2241000
// Madrid: 3165000
// Berlin: 3562000复制代码

对于一个具体问题来讲,咱们的解法并不算差,知足需求、代码也简单,可是它只能正常工做于这样局限的场景中,显然,这不符合函数式思想。数组

咱们从上述代码开始分析,findCityMoreThanOneMillion 函数主要完成了如下三个工做:安全

  1. 过滤:经过 city.population > 1000 过滤出人口超过百万的城市;
  2. 转换单位:经过 city.population * 1000 将单位转换为“个”;
  3. 拼接结果:使用 var result 将结果拼接起来,并最终返回。

这三步天然的帮咱们将原问题分解成了三个子问题,即:闭包

  1. 数组元素过滤问题(Filter);
  2. 数组元素修改问题(Map);
  3. 数组遍历与结果拼接问题(Reduce)。

为了解决原始问题,咱们须要优先解决这三个子问题,很明显,它们对应了标题中的函数,下面一一讨论(为了匹配原书内容,咱们从 Map 开始)。

Map

案例中,咱们须要将 city.population 的单位转换为“个”,本质上就是将一个数值转换为另外一个数值,下面编写一个函数来实现这个功能:

func transformArray(xs: [Int]) -> [Int] {
    var result: [Int] = []
    for x in xs {
        result.append(x * 1000)
    }
    return result
}复制代码

使用该函数能够帮助咱们将一个 [Int] 数组中的每一个元素乘以 1000,这样就能知足咱们从“千”到“个”的单位转换需求,然而,这个函数存在的问题也很是明显:

  1. 入参和返回值均固定为 [Int],扩展性差;
  2. 数值变换方式固定为 x * 1000,场景局限。

试想,若是输入数组可能为 [Double][Int],须要将单位从“千”转换为“万”、“百万”或者“千万”,输出为 [Int][Double],就不得不去修改这个函数,或是添加更多类似的函数。

如何解决呢?先来了解一个概念:泛型(Generics),Swift 官方文档对泛型的定义以下:

Generic code enables you to write flexible, reusable functions and types that can work with any type, subject to requirements that you define. You can write code that avoids duplication and expresses its intent in a clear, abstracted manner.

可见,泛型的目标就是编写灵活、可复用,而且支持任意类型的函数,避免重复性的代码。以官方代码为例:

func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
    let temporaryA = a
    a = b
    b = temporaryA
}

var someInt = 3
var anotherInt = 107
swapTwoValues(&someInt, &anotherInt)
// someInt is now 107, and anotherInt is now 3

var someString = "hello"
var anotherString = "world"
swapTwoValues(&someString, &anotherString)
// someString is now "world", and anotherString is now "hello"复制代码

使用泛型定义的 swapTwoValues 函数可以接受任意类型的入参,不管是 Int 仍是 String 均能正常工做。

回到 transformArray 函数:

引入泛型能够帮助咱们解决第一个问题,即,入参和返回值均固定为 [Int],既然入参和返回值能够是不相关的两种数组类型,那么咱们可使用两个泛型来表示它们,例如 [E][T]

此时,transformArray 函数的入参和返回值变成了 [E][T],那么函数内部所要完成的任务就是将 [E] 转换为 [T],而转换过程正好对应了第二个问题,即,数值变换方式固定位 x * 1000,解决它只须要调用方将这个“转换过程”传递给 transformArray 函数便可,也就是说,咱们须要一个形如这样的函数做为入参:

typealias Transform = (E) -> (T)
// 因为没有定义 E、T 泛型,因此这里仅做示意复制代码

而后,将 transformArray 函数改写以下:

func transformArray<E, T>(xs: [E], transform: Transform) -> [T] {
    var result: [T] = []
    for x in xs {
        result.append(transform(x))
    }
    return result
}复制代码

这样就完成了一个相似 Map 的函数,将这个函数加入 Array 中,并将函数名改成 map,就可使用 Array 对象来调用这个方法了:

extension Array {
    func map<T>(transform: (Element) -> T) -> [T] {
        var result: [T] = []
        for x in self {
            result.append(transform(x))
        }
        return result
    }
}
// 其中 Element 使用 Array 中 Element 泛型定义复制代码

咱们知道,map 函数已经存在于 Swift 标准库中(基于 Sequence 协议实现),所以并不须要本身来实现,咱们经过 Swift 源码来学习一下(路径:swift/stdlib/public/Sequence.swift):

public protocol Sequence {
    ...

    func map<T>(
        _ transform: (Iterator.Element) throws -> T
    ) rethrows -> [T]
}

extension Sequence {
    ...

    public func map<T>(
        _ transform: (Iterator.Element) throws -> T
    ) rethrows -> [T] {
        let initialCapacity = underestimatedCount
        var result = ContiguousArray<T>()
        result.reserveCapacity(initialCapacity)

        var iterator = self.makeIterator()

        // Add elements up to the initial capacity without checking for regrowth.
        for _ in 0..<initialCapacity {
            result.append(try transform(iterator.next()!))
        }
        // Add remaining elements, if any.
        while let element = iterator.next() {
            result.append(try transform(element))
        }
        return Array(result)
    }
}复制代码

除了一些额外的处理,核心部分与咱们的实现是相同的,使用方法以下:

let arr = [10, 20, 30, 40]
let arrMapped = arr.map { $0 % 3 }
print(arrMapped)
// [1, 2, 0, 1]复制代码

Filter

有了 Map 的经验,对于 Filter 的设计就方便多了,咱们参考 transformArray 函数能够这样设计 Filter 函数:

  1. 入参和返回值均为 [T]
  2. 入参的 transform 修改成 isIncluded,类型为 (T) -> Bool,用于判断是否应该包含在返回值中。

实现代码以下:

func filterArray<T>(xs: [T], isIncluded: (T) -> Bool) -> [T] {
    var result: [T] = []
    for x in xs {
        if isIncluded(x) {
            result.append(x)
        }
    }
    return result
}复制代码

一样的,filter 函数也已经存在于 Swift 标准库中,源码以下(路径:swift/stdlib/public/Sequence.swift):

public protocol Sequence {
    ...

    func filter(
        _ isIncluded: (Iterator.Element) throws -> Bool
    ) rethrows -> [Iterator.Element]
}

extension Sequence {
    ...

    public func filter(
        _ isIncluded: (Iterator.Element) throws -> Bool
    ) rethrows -> [Iterator.Element] {

        var result = ContiguousArray<Iterator.Element>()
        var iterator = self.makeIterator()

        while let element = iterator.next() {
            if try isIncluded(element) {
                result.append(element)
            }
        }

        return Array(result)
    }
}复制代码

核心部分实现也是相同的,使用方法以下:

let arr = [10, 20, 30, 40]
let arrFiltered = arr.filter { $0 < 35 }
print(arrFiltered)
// [10, 20, 30]复制代码

Reduce

Reduce 与 Map 不一样之处在于,Map 每次将集合中的元素抛给 transform 闭包,而后获得一个“变形”后的元素,而 Reduce 是将集合中的元素连同当前上下文中的变量一块儿抛给入参闭包(此处命名为 combine),以便于该闭包处理,而后返回处理后的结果,所以 combine 的定义相似:

typealias Combine = (T, E) -> (T)
// 因为没有定义 E、T 泛型,因此这里仅做示意复制代码

所以 reduce 函数能够定义以下:

func reduceArray<E, T>(xs: [E], initial: T, combine: Combine) -> T {
    var result: T = initial
    for x in xs {
        result = combine(result, x)
    }
    return result
}复制代码

Swift reduce 函数源码以下(路径:swift/stdlib/public/Sequence.swift):

/// You rarely need to use iterators directly, because a `for`-`in` loop is the
/// more idiomatic approach to traversing a sequence in Swift. Some
/// algorithms, however, may call for direct iterator use.
///
/// One example is the `reduce1(_:)` method. Similar to the `reduce(_:_:)`
/// method defined in the standard library, which takes an initial value and a
/// combining closure, `reduce1(_:)` uses the first element of the sequence as
/// the initial value.
///
/// Here's an implementation of the `reduce1(_:)` method. The sequence's
/// iterator is used directly to retrieve the initial value before looping
/// over the rest of the sequence.
///
/// extension Sequence {
/// func reduce1(
/// _ nextPartialResult: (Iterator.Element, Iterator.Element) -> Iterator.Element
/// ) -> Iterator.Element?
/// {
/// var i = makeIterator()
/// guard var accumulated = i.next() else {
/// return nil
/// }
///
/// while let element = i.next() {
/// accumulated = nextPartialResult(accumulated, element)
/// }
/// return accumulated
/// }
/// }复制代码

reduce 函数与上面两个函数不太相同,Apple 将其实现以另外一个 reduce1 函数放在了注释中,缘由应该如注释所说,for-in loop 方式更加经常使用,但咱们仍然能够正常使用 reduce 函数,方法以下:

let arr = [10, 20, 30, 40]
let arrReduced = arr.reduce(output) { result, x in
    return result + "\(x) "
}
print(arrReduced)
// Arr contains 10 20 30 40复制代码

函数式解决方案

在准备好了 Map、Filter 和 Reduce 工具库以后,咱们再来解决 City Filter 问题:

let result =
    cities.filter { $0.population > 1000 }
        .map { $0.cityByScalingPopulation() }
        .reduce("City: Population") { result, c in
            return result + "\n" + "\(c.name): \(c.population)"
        }
print(result)
// City: Population
// Paris: 2241000
// Madrid: 3165000
// Berlin: 3562000

extension City {
    func cityByScalingPopulation() -> City {
        return City(name: name, population: population * 1000)
    }
}复制代码

借助 Map、Filter 和 Reduce 等方法,能够方便的使用链式语法对原数组进行处理,并获得最终结果。


思考

函数式思想

当咱们讨论函数式思想时,咱们到底在说什么?

简单说,函数式思想是经过构建一系列简单、实用的函数,再“装配”起来解决实际问题,对于这句话的理解,我想至少有三点:

  1. 目标转换:基于函数式思想解决问题时,目标再也不“急功近利”直接解决具体问题,而是庖丁解牛,把具体问题分解成为小规模的,甚至是互不相干的子模块,攻克这些子模块才是更高优先级的工做;
  2. 函数设计:分解出的每一个子模块,实际上也就对应了一个、或一组可以独立工做的函数,良好的函数设计不只有助于咱们解决当前问题,更能为咱们构建一个优秀的工具库,去解决不少其余问题;
  3. 问题解决:有时具体问题的解决好像已经被咱们遗忘了,在解决了子问题、构建了工具库后,简单“装配”就能轻松解决原始问题。借助函数式思想,咱们也更容易发现问题之间的共同点,从而快速解决,换句话说,解决问题成为了函数式思想下的“副产品”

泛型

Swift 中对于泛型的应用很是普遍,使用泛型可以使咱们事半功倍,一个函数能够“瞬间”支持几乎全部类型,更重要的是,由于 Swift 语言的“类型安全”特性,使得这一切都安全可靠。

泛型之因此安全,是由于它仍然处于编译器的类型控制下,而 Swift 中的 Any 类型就不那么安全了,表面上看二者都能表示任意类型,但使用 Any 类型可以避开编译器的检查,从而可能形成错误,来看下面的例子:

func exchange<T>(_ income: T) -> T {
    return "Money: \(income)" // error
}

func exchangeAny(_ income: Any) -> Any {
    return "Money: \(income)"
}复制代码

一样的函数体,使用泛型的 exchange 会提示错误:error: cannot convert return expression of type 'String' to return type 'T',而使用 AnyexchangeAny 则不提示任何错误。若是咱们不清楚 exchangeAny 的返回值类型,而直接调用,则可能致使运行时错误,是很是危险的。所以,善用泛型可以让咱们在“无须牺牲类型安全就可以在编译器的帮助下写出灵活的函数”。

更多关于泛型的讨论请参阅原书,或官方文档。


参考资料

  1. Github: objcio/functional-swift
  2. The Swift Programming Language: Generics
  3. The Swift Programming Language (Source Code)

本文属于《函数式 Swift》读书笔记系列,同步更新于 huizhao.win,欢迎关注!