https://blog.csdn.net/offbye/article/details/50856101?locationNum=4&fps=1
即使早在 Swift 正式發布之前,iOS / Cocoa 開發者都可以使用諸如 ObjectiveSugar 或者 ReactiveCocoa 第三方庫,實現類似map
、flatMap
或 filter
等函數式編程的構建。而在 Swift 中,這些家伙(map
等幾個函數)已經入駐成為「頭等公民」了。比起標准的 for
循環,使用函數式編程有很多優勢。它們通常能夠更好地表達你的意圖,減少代碼的行數,以及使用鏈式結構構建復雜的邏輯,更顯清爽。
本文中,我將介紹附加於 Swift 中的一個非常酷的函數:「Reduce」。相對於 map
/ filter
函數,reduce
有時不失為一個更好的解決方案。
一個簡單的問題
思考這么一個問題:你從 JSON 中獲取到一個 persons 列表,意圖計算所有來自 California 的居民的平均年齡。需要解析的數據如下所示:
-
let persons: [[String: String]] = [[ "name": "Carl Saxon", "city": "New York, NY", "age": "44"],
-
[ "name": "Travis Downing", "city": "El Segundo, CA", "age": "34"],
-
[ "name": "Liz Parker", "city": "San Francisco, CA", "age": "32"],
-
[ "name": "John Newden", "city": "New Jersey, NY", "age": "21"],
-
[ "name": "Hector Simons", "city": "San Diego, CA", "age": "37"],
-
[ "name": "Brian Neo", "age": "27"]] //注意這家伙沒有 city 鍵值
注意最后一個記錄,它遺漏了問題中 person 的居住地 city 。對於這些情況,默默忽略即可…
本例中,我們期望的結果是那三位來自 California 的居民。讓我們嘗試在 Swift 中使用 flatMap
和 filter
來實現這個任務。使用 flatMap
函數替代 map
函數的原因在於前者能夠忽略可選值為 nil 的情況。例如 flatMap([0,nil,1,2,nil])
的結果是 [0,1,2]
。處理那些沒有 city 屬性的情況這會非常有用。
-
func infoFromState(state state: String, persons: [[String: AnyObject]])
-
-> Int {
-
// 先進行 flatMap 后進行 filter 篩選
-
// $0["city"] 是一個可選值,對於那些沒有 city 屬性的項返回 nil
-
// componentsSeparatedByString 處理鍵值,例如 "New York, NY"
-
// 最后返回的 ["New York","NY"],last 取到最后的 NY
-
return persons.flatMap( { $0["city"] })
-
.filter({$ 0.hasSuffix(state)})
-
.count
-
}
-
infoFromState(state: "CA", persons: persons)
-
//#+RESULTS:
-
//: 3
這非常簡單。
不過,現在來思考另外一個難題:你想要獲悉居住在 California 的人口數,接着計算他們的平均年齡。如果我們想要在上面函數的基礎上嘗試做修改,立馬會發現難度不小。解決方法倒是有幾種,不過大都看起來不適用函數式結構解決方案。倒是通過循環的方式能簡單的解決這個問題。
這時候我們要琢磨為啥不適用了,原因很簡單:數據的形式(Shape)改變了。而 map
、flatMap
和 filter
函數能夠始終保持數據形式的相似性。數組傳入,數組返回。當然數組的元素個數和內容可以改變,不過始終是數組形式(Array-shape)。但是,上面所描述的問題要求我們最后轉換成的結果是個結構體(Struct),或者說是以元組(Tuple)的形式包含一個整型平均值(平均年齡)和一個整型總和(人口數)。
對於這種類型的問題,我們可以使用 reduce
來救場。
Reduce
Reduce 是 map
、flatMap
或 filter
的一種擴展的形式(譯者注:后三個函數能干嘛,reduce 就能用另外一種方式實現)。Reduce 的基礎思想是將一個序列轉換為一個不同類型的數據,期間通過一個累加器(Accumulator)來持續記錄遞增狀態。為了實現這個方法,我們會向 reduce 方法中傳入一個用於處理序列中每個元素的結合(Combinator)閉包 / 函數 / 方法。這聽起來有點復雜,不過通過幾個例子練手,你就會發現這相當簡單。
它是 SequenceType
中的一個方法,看起來是這樣的(簡化版本):
func reduce<T>(initial: T, combine: (T, Self.Generator.Element) -> T) -> T |
此刻,我們擁有一個初始值(Initial value)以及一個閉包(返回值類型和初始值類型一致)。函數最后的返回值同樣和初始值類型一致,為 T
。
假設我們現在要實現一個 reduce 操作 — 對一個整數列表值做累加運算,方案如下:
func combinator(accumulator: Int, current: Int) -> Int { |
[1, 2, 3]
中的每個元素都將調用一次結合(Combinator)函數進行處理。同時我們使用累加器(Accumulator)變量實時記錄遞增狀態(遞增並非是指加法),這里是一個整型值。
接下來,我們重新實現那些函數式編程的「伙伴」(自己來寫 map、flatMap 和 filter 函數)。簡便起見,所有這些方法都是對Int
或 Optional<Int>
進行操作的;換言之,我們此刻不考慮泛型。另外牢記下面的實現只是為了展示 reduce
的實現過程。原生的 Swift 實現相比較下面 reduce 的版本,速度要快很多1。不過,Reduce 能在不同的問題中表現得很好,之后會進一步地詳述。
Map
// 重新定義一個 map 函數 |
這個例子能夠很好地幫助你理解 reduce
的基礎知識。
- 首先,elements 序列調用 reduce 方法:
elements.reduce...
。 - 然后,我們傳入初始值給累加器(Accumulator),即一個 Int 類型空數組(
[Int]()
)。 - 接着,我們傳入
combinator
閉包,它接收兩個參數:第一個參數為 accumulator,即acc: [Int]
;第二個參數為從序列中取得的當前對象obj: Int
(譯者注:對序列進行遍歷,每次取到其中的一個對象 obj)。 combinator
閉包體中的實現代碼非常簡單。我們對 obj 做變換處理,然后添加到累加器 accumulator 中。最后返回 accumulator 對象。
相比較調用 map
方法,這種實現代碼看起來有點冗余。的確如此!但是,上面這個版本相當詳細地解釋了 reduce
方法是怎么工作的。我們可以對此進行簡化。
func rmap(elements: [Int], transform: (Int) -> Int) -> [Int] { |
依舊能夠正常運行。這個版本都有哪些不同呢?實際上,我們使用了 Swift 中的小技巧,+
運算符能夠對兩個序列進行加法操作。因此 [0, 1, 2] + [transform(4)]
表達式將左序列和右序列進行相加,其中右序列由轉換后的元素構成。
這里有個地方需要引起注意:[0, 1, 2] + [4]
執行速度要慢於 [0, 1, 2].append(4)
。倘若你正在處理龐大的列表,應取代集合 + 集合的方式,轉而使用一個可變的 accumulator 變量進行遞增:
func rmap(elements: [Int], transform: (Int) -> Int) -> [Int] { |
為了進一步加深對 reduce
的理解,我們將繼續重新實現 flatMap
和 filter
方法。
func rflatMap(elements: [Int], transform: (Int) -> Int?) -> [Int] { |
這里 rflatMap 和 rmap 主要差異在於,前者增加了一個 guard
表達式確保可選類型始終有值(換言之,摒棄那些 nil 的情況)。
Filter
func rFilter(elements: [Int], filter: (Int) -> Bool) -> [Int] { |
依舊難度不大。我們再次使用 guard 表達式確保滿足篩選條件。
到目前為止,reduce
方法看起來更像是 map
或 filter
的復雜版本,除此之外然並卵。不過,所結合的內容不需要是一個數組,它可以是其他任何類型。這使得我們依靠一種簡單的方式,就可以輕松地實現各種 reduction 操作。
Reduce 范例
首先介紹我最喜歡的數組元素求和范例:
// 初始值 initial 為 0,每次遍歷數組元素,執行 + 操作 |
僅傳入 +
作為一個 combinator
函數是有效的,它僅僅是對 lhs(Left-hand side,等式左側)
和rhs(Right-hand side,等式右側)
做加法處理,最后返回結果值,這完全滿足 reduce
函數的要求。
另外一個范例:通過一組數字計算他們的乘積:
// 初始值 initial 為 1,每次遍歷數組元素,執行 * 操作 |
甚至我們可以反轉數組:
// $0 指累加器(accumulator),$1 指遍歷數組得到的一個元素 |
最后,來點有難度的任務。我們想要基於某個標准對列表做划分(Partition)處理:
// 為元組定義個別名,此外 Acc 也是閉包傳入的 accumulator 的類型 |
上面實現中最有意思的莫過於我們使用 tuple
作為 accumulator。你會漸漸發現,一旦你嘗試將 reduce
進入到日常工作流中,tuple
是一個不錯的選擇,它能夠將數據與 reduce 操作快速掛鈎起來。
執行效率對比:Reduce vs. 鏈式結構
reduce
除了較強的靈活性之外,還具有另一個優勢:通常情況下,map
和 filter
所組成的鏈式結構會引入性能上的問題,因為它們需要多次遍歷你的集合才能最終得到結果值,這種操作往往伴隨着性能損失,比如以下代碼:
[0, 1, 2, 3, 4].map({ $0 + 3}).filter({ $0 % 2 == 0}).reduce(0, combine: +) |
除了毫無意義之外,它還浪費了 CPU 周期。初始序列(即 [0, 1, 2, 3, 4])被重復訪問了三次之多。首先是 map,接着 filter,最后對數組內容求和。其實,所有這一切操作我們能夠使用 reduce
完全替換實現,極大提高執行效率:
// 這里只需要遍歷 1 次序列足矣 |
這里給出一個快速的基准運行測試,使用以上兩個版本以及 for-loop 方式對一個容量為 100000 的列表做處理操作:
// for-loop 版本 |
正如你所看見的,reduce
版本的執行效率和 for-loop
操作非常相近,且是鏈式操作的一半時間。
不過,在某些情況中,鏈式操作是優於 reduce
的。思考如下范例:
Array(0...100000).map({ $0 + 3}).reverse().prefix(3) |
Array(0...100000).reduce([], combine: { (var ac: [Int], r: Int) -> [Int] in |
這里,注意到使用鏈式操作花費 0.027s,這與 reduce 操作的 2.927s 形成了鮮明的反差,這究竟是怎么回事呢?2
Reddit 網站的搜索結果指出,從 reduce 的語義上來說,傳入閉包的參數(如果可變的話,即 mutated),會對底層序列的每個元素都產生一份 copy 。在我們的案例中,這意味着 accumulator 參數 ac
將為 0…100000 范圍內的每個元素都執行一次復制操作。有關對此更好、更詳細的解釋請看這篇 Airspeedvelocity 博客文章。
因此,當我們試圖使用 reduce
來替換掉一組操作時,請時刻保持清醒,問問自己:reduction 在問題中的情形下是否確實是最合適的方式。
現在,可以回到我們的初始問題:計算人口總數和平均年齡。請試着用 reduce
來解決吧。
再一次嘗試來寫 infoFromState 函數
-
func infoFromState(state state: String, persons: [[String: AnyObject]])
-
-> (count: Int, age: Float) {
-
-
// 在函數內定義別名讓函數更加簡潔
-
typealias Acc = (count: Int, age: Float)
-
-
// reduce 結果暫存為臨時的變量
-
let u = persons.reduce((count: 0, age: 0.0)) {
-
(ac: Acc, p) -> Acc in
-
-
// 獲取地區和年齡
-
guard let personState = p[ "city"],
-
personAge = p[ "age"]
-
// 確保選出來的是來自正確的洲
-
where personState.hasSuffix(state)
-
-
-
// 如果缺失年齡或者地區,又或者上者比較結果不等,返回
-
else { return ac }
-
-
// 最終累加計算人數和年齡
-
return (count: ac.count + 1, age: ac.age + Float(personAge))
-
}
-
-
// 我們的結果就是上面的人數和除以人數后的平均年齡
-
return (age: u.age / Float(u.count), count: u.count)
-
}
-
print(infoFromState(state: "CA", persons: persons))
-
// prints: (count: 3, age: 34.3333)
和早前的范例一樣,我們再次使用了 tuple
作為 accumulator 記錄狀態值。除此之外,代碼讀起來簡明易懂。
同時,我們在函數體中定義了一個別名 Acc:typealias Acc = (count: Int, age: Float)
,起到了簡化類型注釋的作用。
總結
本文是對 reduce
方法的一個簡短概述。倘若你不想將過多函數式方法通過鏈式結構串聯起來調用,亦或是數據的輸出形式與傳入數據的形式不一致時,reduce 就相當有用了。最后,我將向你展示通過使用 reduce 的各種范例來結束本文,希望能為你帶來些許靈感。
更多范例
以下范例展示了 reduce
的其他使用案例。請記住例子只作為展示教學使用,即它們更多地強調 reduce 的使用方式,而非為你的代碼庫提供通用的解決方法。大多數范例都可以通過其他更好、更快的方式來編寫(即通過 extension 或 generics)。並且這些實現方式已經在許多 Swift 庫中都有實現,諸如 SwiftSequence 以及 Dollar.swift
Minimum
返回列表中的最小項。顯然,[1, 5, 2, 9, 4].minElement()
方法更勝一籌。
// 初始值為 Int.max,傳入閉包為 min:求兩個數的最小值 |
Unique
剔除列表中重復的元素。當然,最好的解決方式是使用集合(Set)
。
[1, 2, 5, 1, 7].reduce([], combine: { (a: [Int], b: Int) -> [Int] in |
Group By
遍歷整個列表,通過一個鑒別函數對列表中元素進行分組,將分組后的列表作為結果值返回。問題中的鑒別函數返回值類型需要遵循 Hashable
協議,這樣我們才能擁有不同的鍵值。此外保留元素的排序,而組內元素排序則不一定被保留下來。
func groupby<T, H: Hashable>(items: [T], f: (T) -> H) -> [H: [T]] { |
Interpose
函數給定一個 items
數組,每隔 count
個元素插入 element
元素,返回結果值。下面的實現確保了 element 僅在中間插入,而不會添加到數組尾部。
func interpose<T>(items: [T], element: T, count: Int = 1) -> [T] { |
Interdig
該函數允許你有選擇從兩個序列中挑選元素合並成為一個新序列返回。
func interdig<T>(list1: [T], list2: [T]) -> [T] { |
Chunk
該函數返回原數組分解成長度為 n
后的多個數組:
func chunk<T>(list: [T], length: Int) -> [[T]] { |
函數中使用一個更為復雜的 accumulator
,包含了 stack、current list 以及 count 。
譯者注:有關 Reduce 底層實現,請看這篇文章。
原文 http://swift.gg/2015/12/10/reduce-all-the-things/