今天咱们加入了乌特勒支大学助理教授Wouter Swierstra,他是Functional Swift的合着者。他工做的一个领域是函数式编程,他很高兴看到来自这个领域的一些想法,人们已经在很长一段时间内工做,正在成为像Swift这样的主流语言。编程
咱们将在几集中共同探讨函数式编程的兔子洞。更具体地说,咱们将关注reduce
。为了提醒本身是什么reduce
,咱们先写一些例子。swift
咱们建立一个数组,用于保存从1到10的数字,咱们调用reduce
它来查找数组中的最大数字。该函数有两个参数:初始结果值,以及将单个数组元素与结果组合在一块儿的函数。咱们传入尽量小Int
的初始值,咱们max
用于组合函数:数组
let numbers = Array(1...10)
numbers.reduce(Int.min, max) // 10
复制代码
咱们还能够reduce
经过传入零和+
运算符来计算全部元素的总和:bash
numbers.reduce(0, +) // 55
复制代码
让咱们仔细看看reduce
on 的函数签名Array
:函数式编程
func reduce<Result>(_ initialResult: Result, _ nextPartialResult: (Result, Self.Element) throws -> Result) rethrows -> Result
复制代码
该函数在其Result
类型上是通用的。在上面两个例子中,结果类型和数组元素的Int
类型都是,但这些类型没必要匹配。例如,咱们还能够reduce
用来肯定数组是否包含元素。这个reduce
电话的结果是Bool
:函数
extension Sequence where Element: Equatable {
func contains1(_ el: Element) -> Bool {
return reduce(false) { result, x in
return x == el || result
}
}
}
numbers.contains1(3) // true
numbers.contains1(13) // false
复制代码
咱们调用reduce
初始结果false
,由于若是数组为空,这必须是结果。在组合函数中,咱们检查传入的元素是否等于咱们正在寻找的元素,或者到目前为止的结果是否相等true
。ui
这个版本contains
不是最高效的,由于它作的工做比它须要的多。然而,找到一个使用的实现是一个有趣的练习reduce
。spa
可是reduce
从哪里来的?咱们能够经过定义单链表并reduce
在其上查找操做来探索其起源。code
在Swift中,咱们将链表定义为枚举,其中包含空列表的大小写和非空列表的大小写。传统上称为非空状况cons
,其关联值是单个列表元素和尾部。尾部是另外一个列表,它使案例递归,所以咱们必须将其标记为间接:递归
enum List<Element> {
case empty
indirect case cons(Element, List)
}
复制代码
咱们能够建立一个整数列表,以下所示:
let list: List<Int> = .cons(1, .cons(2, .const(3, .empty)))
复制代码
而后咱们定义一个名为的函数fold
,看起来很像reduce
,但它有点不一样:
extension List {
func fold<Result>(_ emptyCase: Result, _ consCase: (Element, Result) -> Result) -> Result {
}
}
复制代码
这两个论点fold
与两个案例相匹配并非偶然的List
。在函数的实现中,咱们使用每一个参数及其相应的大小写:
extension List {
func fold<Result>(_ emptyCase: Result, _ consCase: (Element, Result) -> Result) -> Result {
switch self {
case .empty:
return emptyCase
case let .cons(x, xs):
return consCase(x, xs.fold(emptyCase, consCase))
}
}
}
复制代码
如今咱们能够fold
在列表中计算其元素的总和:
list.fold(0, +) // 6
复制代码
咱们还能够fold
用来查找列表的长度:
list.fold(0, { _, result in result + 1 }) // 3
复制代码
在论证fold
和宣言之间存在对应关系List
。
咱们能够将enum案例List
视为构造列表的两种方法:一种是构造一个空列表,另外一种是构造一个非空列表。
而且fold
有两个参数:一个用于.empty
案例,一个用于.cons
案例 - 正是咱们为了计算每一个案例的结果所需的信息。
若是咱们认为emptyCase
参数不是类型的值Result
,而是做为函数() -> Result
,那么与.empty
构造函数的对应关系变得更加清晰。
的fold
功能几乎是相同的reduce
,但有一个小的区别。能够经过调用两个函数并比较结果来证实二者之间的差别。
首先咱们调用fold
,传递两个案例的构造函数List
做为参数:
dump(list.fold(List.empty, List.cons))
/*
▿ __lldb_expr_4.List<Swift.Int>.cons
▿ cons: (2 elements)
- .0: 1
▿ .1: __lldb_expr_4.List<Swift.Int>.cons
▿ cons: (2 elements)
- .0: 2
▿ .1: __lldb_expr_4.List<Swift.Int>.cons
▿ cons: (2 elements)
- .0: 3
- .1: __lldb_expr_4.List<Swift.Int>.empty
*/
复制代码
咱们看到结果与原始列表彻底相同。换句话说,fold
使用两个case构造函数调用是一种编写身份函数的复杂方法:没有任何改变。
而后咱们reduce
一个数组,传入相同的构造函数List
- 除了咱们必须交换cons
case 的参数的顺序,由于首先reduce
传递累积结果而第二个传递当前元素:
dump(Array(1...3).reduce(List.empty, { .cons($1, $0) }))
/*
▿ __lldb_expr_6.List<Swift.Int>.cons
▿ cons: (2 elements)
- .0: 3
▿ .1: __lldb_expr_6.List<Swift.Int>.cons
▿ cons: (2 elements)
- .0: 2
▿ .1: __lldb_expr_6.List<Swift.Int>.cons
▿ cons: (2 elements)
- .0: 1
- .1: __lldb_expr_6.List<Swift.Int>.empty
*/
复制代码
当咱们检查这个reduce
调用的结果时,咱们看到它List
是以相反的顺序包含数组元素,由于reduce
遍历数组并将每一个元素处理成结果。这与什么不一样fold
,由于它从左到右穿过链表,而且仅emptyCase
当它到达列表的最末端时才使用该值。
有不少操做,好比计算总和或长度,reduce
并fold
给出相同的结果。可是经过秩序重要的操做,咱们开始看到两个函数的行为差别。
咱们已经实现了fold
,咱们已经使用过Swift Array.reduce
,但看看它的实现也颇有意思List.reduce
。咱们在扩展中编写函数,并给它们相同的参数fold
:
extension List {
func reduce<Result>(_ emptyCase: Result, _ consCase: (Element, Result) -> Result) -> Result {
// ...
}
}
复制代码
为了实现该功能,咱们将emptyCase
参数分配给初始结果,而后咱们切换列表以查看它是否为空。若是它是空的,咱们能够当即返回结果。若是列表是非空的,咱们将x
元素添加到咱们到目前为止使用consCase
函数看到的结果中,而且咱们递归调用reduce
尾部,传递累积的结果:
extension List {
func reduce<Result>(_ emptyCase: Result, _ consCase: (Element, Result) -> Result) -> Result {
let result = emptyCase
switch self {
case .empty:
return result
case let .cons(x, xs):
return xs.reduce(consCase(x, result), consCase)
}
}
}
复制代码
在这里咱们能够看到它reduce
是尾递归的:它要么返回一个结果,要么当即进行递归调用。fold
不是尾递归,由于它调用consCase
函数,而且递归或多或少被隐藏并用于构造该函数的第二个参数。
这种差别致使了不一样的结果,如今经过比较两种方法咱们能够更清楚地看到List
:
let list: List<Int> = .cons(1, .cons(2, .const(3, .empty)))
list.fold(List.empty, List.cons) // .cons(1, .cons(2, .const(3, .empty)))
list.reduce(List.empty, List.cons) // .cons(3, .cons(2, .const(1, .empty)))`
复制代码
使用尾递归的操做能够很容易地用循环重写:
extension List {
func reduce1<Result>(_ emptyCase: Result, _ consCase: (Element, Result) -> Result) -> Result {
var result = emptyCase
var copy = self
while case let .cons(x, xs) = copy {
result = consCase(x, result)
copy = xs
}
return result
}
}
复制代码
这个版本reduce1
产生的结果与reduce
:
list.reduce1(List.empty, List.cons) // .cons(3, .cons(2, .cons(1, .empty)))
复制代码
reduce
只是折叠操做的一个例子,咱们实际上也能够在许多其余结构上定义这些操做。