今天咱們加入了烏特勒支大學助理教授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
只是摺疊操做的一個例子,咱們實際上也能夠在許多其餘結構上定義這些操做。