说明:本文及所属系列文章为图书《函数式 Swift》的读书笔记,旨在记录学习过程、分享学习心得。文中部分代码摘自原书开源代码库 Github: objcio/functional-swift,部份内容摘自原书。如需深刻学习,请购买正版支持原书。(受 @SwiftLanguage 微博启发,特此说明)javascript
标题中的三个数组操做函数咱们并不陌生,本章将借助这些 Swift 标准库中的函数,再次探索函数式思想的应用。html
请带着如下关键词阅读本文:java
使用 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
函数主要完成了如下三个工做:安全
city.population > 1000
过滤出人口超过百万的城市;city.population * 1000
将单位转换为“个”;var result
将结果拼接起来,并最终返回。这三步天然的帮咱们将原问题分解成了三个子问题,即:闭包
为了解决原始问题,咱们须要优先解决这三个子问题,很明显,它们对应了标题中的函数,下面一一讨论(为了匹配原书内容,咱们从 Map 开始)。
案例中,咱们须要将 city.population
的单位转换为“个”,本质上就是将一个数值转换为另外一个数值,下面编写一个函数来实现这个功能:
func transformArray(xs: [Int]) -> [Int] {
var result: [Int] = []
for x in xs {
result.append(x * 1000)
}
return result
}复制代码
使用该函数能够帮助咱们将一个 [Int]
数组中的每一个元素乘以 1000,这样就能知足咱们从“千”到“个”的单位转换需求,然而,这个函数存在的问题也很是明显:
[Int]
,扩展性差;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]复制代码
有了 Map 的经验,对于 Filter 的设计就方便多了,咱们参考 transformArray
函数能够这样设计 Filter 函数:
[T]
;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 与 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 等方法,能够方便的使用链式语法对原数组进行处理,并获得最终结果。
当咱们讨论函数式思想时,咱们到底在说什么?
简单说,函数式思想是经过构建一系列简单、实用的函数,再“装配”起来解决实际问题,对于这句话的理解,我想至少有三点:
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',而使用 Any
的 exchangeAny
则不提示任何错误。若是咱们不清楚 exchangeAny
的返回值类型,而直接调用,则可能致使运行时错误,是很是危险的。所以,善用泛型可以让咱们在“无须牺牲类型安全就可以在编译器的帮助下写出灵活的函数”。
更多关于泛型的讨论请参阅原书,或官方文档。
本文属于《函数式 Swift》读书笔记系列,同步更新于 huizhao.win,欢迎关注!