Swift化零為整:Reduce 詳解


https://blog.csdn.net/offbye/article/details/50856101?locationNum=4&fps=1

即使早在 Swift 正式發布之前,iOS / Cocoa 開發者都可以使用諸如 ObjectiveSugar 或者 ReactiveCocoa 第三方庫,實現類似mapflatMap 或 filter 等函數式編程的構建。而在 Swift 中,這些家伙(map 等幾個函數)已經入駐成為「頭等公民」了。比起標准的 for 循環,使用函數式編程有很多優勢。它們通常能夠更好地表達你的意圖,減少代碼的行數,以及使用鏈式結構構建復雜的邏輯,更顯清爽。

本文中,我將介紹附加於 Swift 中的一個非常酷的函數:「Reduce」。相對於 map / filter 函數,reduce 有時不失為一個更好的解決方案。

一個簡單的問題

思考這么一個問題:你從 JSON 中獲取到一個 persons 列表,意圖計算所有來自 California 的居民的平均年齡。需要解析的數據如下所示:

 

  1.  
    let persons: [[String: String]] = [[ "name": "Carl Saxon", "city": "New York, NY", "age": "44"],
  2.  
    [ "name": "Travis Downing", "city": "El Segundo, CA", "age": "34"],
  3.  
    [ "name": "Liz Parker", "city": "San Francisco, CA", "age": "32"],
  4.  
    [ "name": "John Newden", "city": "New Jersey, NY", "age": "21"],
  5.  
    [ "name": "Hector Simons", "city": "San Diego, CA", "age": "37"],
  6.  
    [ "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 屬性的情況這會非常有用。

 

  1.  
    func infoFromState(state state: String, persons: [[String: AnyObject]])
  2.  
    -> Int {
  3.  
    // 先進行 flatMap 后進行 filter 篩選
  4.  
    // $0["city"] 是一個可選值,對於那些沒有 city 屬性的項返回 nil
  5.  
    // componentsSeparatedByString 處理鍵值,例如 "New York, NY"
  6.  
    // 最后返回的 ["New York","NY"],last 取到最后的 NY
  7.  
    return persons.flatMap( { $0["city"] })
  8.  
    .filter({$ 0.hasSuffix(state)})
  9.  
    .count
  10.  
    }
  11.  
    infoFromState(state: "CA", persons: persons)
  12.  
    //#+RESULTS:
  13.  
    //: 3



 

這非常簡單。

不過,現在來思考另外一個難題:你想要獲悉居住在 California 的人口數,接着計算他們的平均年齡。如果我們想要在上面函數的基礎上嘗試做修改,立馬會發現難度不小。解決方法倒是有幾種,不過大都看起來不適用函數式結構解決方案。倒是通過循環的方式能簡單的解決這個問題。

這時候我們要琢磨為啥不適用了,原因很簡單:數據的形式(Shape)改變了。而 mapflatMap 和 filter 函數能夠始終保持數據形式的相似性。數組傳入,數組返回。當然數組的元素個數和內容可以改變,不過始終是數組形式(Array-shape)。但是,上面所描述的問題要求我們最后轉換成的結果是個結構體(Struct),或者說是以元組(Tuple)的形式包含一個整型平均值(平均年齡)和一個整型總和(人口數)。

對於這種類型的問題,我們可以使用 reduce 來救場。

Reduce

Reduce 是 mapflatMap 或 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 {
return accumulator + current
}
[1, 2, 3].reduce(0, combine: combinator)
// 執行步驟如下
combinator(0, 1) { return 0 + 1 } = 1
combinator(1, 2) { return 1 + 2 } = 3
combinator(3, 3) { return 3 + 3 } = 6
= 6

[1, 2, 3] 中的每個元素都將調用一次結合(Combinator)函數進行處理。同時我們使用累加器(Accumulator)變量實時記錄遞增狀態(遞增並非是指加法),這里是一個整型值。

接下來,我們重新實現那些函數式編程的「伙伴」(自己來寫 map、flatMap 和 filter 函數)。簡便起見,所有這些方法都是對Int 或 Optional<Int> 進行操作的;換言之,我們此刻不考慮泛型。另外牢記下面的實現只是為了展示 reduce 的實現過程。原生的 Swift 實現相比較下面 reduce 的版本,速度要快很多1。不過,Reduce 能在不同的問題中表現得很好,之后會進一步地詳述。

Map

// 重新定義一個 map 函數
func rmap(elements: [Int], transform: (Int) -> Int) -> [Int] {
return elements.reduce([Int](), combine: { (var acc: [Int], obj: Int) -> [Int] in
acc.append(transform(obj))
return acc
})
}
print(rmap([1, 2, 3, 4], transform: { $0 * 2}))
// [2, 4, 6, 8]

這個例子能夠很好地幫助你理解 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] {
// $0 表示第一個傳入參數,$1 表示第二個傳入參數,依次類推...
return elements.reduce([Int](), combine: {$0 + [transform($1)]})
}
print(rmap([1, 2, 3, 4], transform: { $0 * 2}))
// [2, 4, 6, 8]

依舊能夠正常運行。這個版本都有哪些不同呢?實際上,我們使用了 Swift 中的小技巧,+ 運算符能夠對兩個序列進行加法操作。因此 [0, 1, 2] + [transform(4)] 表達式將左序列和右序列進行相加,其中右序列由轉換后的元素構成。

這里有個地方需要引起注意:[0, 1, 2] + [4] 執行速度要慢於 [0, 1, 2].append(4)。倘若你正在處理龐大的列表,應取代集合 + 集合的方式,轉而使用一個可變的 accumulator 變量進行遞增:

func rmap(elements: [Int], transform: (Int) -> Int) -> [Int] {
return elements.reduce([Int](), combine: { (var ac: [Int], b: Int) -> [Int] in
// 作者提倡使用這種,因為執行速度更快
ac.append(transform(b))
return ac
})
}

為了進一步加深對 reduce 的理解,我們將繼續重新實現 flatMap 和 filter 方法。

func rflatMap(elements: [Int], transform: (Int) -> Int?) -> [Int] {
return elements.reduce([Int](),
combine: { guard let m = transform($1) else { return $0 }
return $0 + [m]})
}
print(rflatMap([1, 3, 4], transform: { guard $0 != 3 else { return nil }; return $0 * 2}))
// [2, 8]

這里 rflatMap 和 rmap 主要差異在於,前者增加了一個 guard 表達式確保可選類型始終有值(換言之,摒棄那些 nil 的情況)。

Filter

func rFilter(elements: [Int], filter: (Int) -> Bool) -> [Int] {
return elements.reduce([Int](),
combine: { guard filter($1) else { return $0 }
return $0 + [$1]})
}
print(rFilter([1, 3, 4, 6], filter: { $0 % 2 == 0}))
// [4, 6]

依舊難度不大。我們再次使用 guard 表達式確保滿足篩選條件。

到目前為止,reduce 方法看起來更像是 map 或 filter 的復雜版本,除此之外然並卵。不過,所結合的內容不需要是一個數組,它可以是其他任何類型。這使得我們依靠一種簡單的方式,就可以輕松地實現各種 reduction 操作。

Reduce 范例

首先介紹我最喜歡的數組元素求和范例:

// 初始值 initial 為 0,每次遍歷數組元素,執行 + 操作
[0, 1, 2, 3, 4].reduce(0, combine: +)
// 10

僅傳入 + 作為一個 combinator 函數是有效的,它僅僅是對 lhs(Left-hand side,等式左側) 和rhs(Right-hand side,等式右側) 做加法處理,最后返回結果值,這完全滿足 reduce 函數的要求。

另外一個范例:通過一組數字計算他們的乘積:

// 初始值 initial 為 1,每次遍歷數組元素,執行 * 操作
[1, 2, 3, 4].reduce(1, combine: *)
// 24

甚至我們可以反轉數組:

// $0 指累加器(accumulator),$1 指遍歷數組得到的一個元素
[1, 2, 3, 4, 5].reduce([Int](), combine: { [$1] + $0 })
// 5, 4, 3, 2, 1

最后,來點有難度的任務。我們想要基於某個標准對列表做划分(Partition)處理:

// 為元組定義個別名,此外 Acc 也是閉包傳入的 accumulator 的類型
typealias Acc = (l: [Int], r: [Int])
func partition(lst: [Int], criteria: (Int) -> Bool) -> Acc {
return lst.reduce((l: [Int](), r: [Int]()), combine: { (ac: Acc, o: Int) -> Acc in
if criteria(o) {
return (l: ac.l + [o], r: ac.r)
} else {
return (r: ac.r + [o], l: ac.l)
}
})
}
partition([1, 2, 3, 4, 5, 6, 7, 8, 9], criteria: { $0 % 2 == 0 })
//: ([2, 4, 6, 8], [1, 3, 5, 7, 9])

上面實現中最有意思的莫過於我們使用 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 次序列足矣
[0, 1, 2, 3, 4].reduce(0, combine: { (ac: Int, r: Int) -> Int in
if (r + 3) % 2 == 0 {
return ac + r + 3
} else {
return ac
}
})

這里給出一個快速的基准運行測試,使用以上兩個版本以及 for-loop 方式對一個容量為 100000 的列表做處理操作:

// for-loop 版本
var ux = 0
for i in Array(0...100000) {
if (i + 3) % 2 == 0 {
ux += (i + 3)
}
}

測試結果測試結果

正如你所看見的,reduce 版本的執行效率和 for-loop 操作非常相近,且是鏈式操作的一半時間。

不過,在某些情況中,鏈式操作是優於 reduce 的。思考如下范例:

Array(0...100000).map({ $0 + 3}).reverse().prefix(3)
// 0.027 Seconds

 

Array(0...100000).reduce([], combine: { (var ac: [Int], r: Int) -> [Int] in
ac.insert(r + 3, atIndex: 0)
return ac
}).prefix(3)
// 2.927 Seconds

這里,注意到使用鏈式操作花費 0.027s,這與 reduce 操作的 2.927s 形成了鮮明的反差,這究竟是怎么回事呢?2

Reddit 網站的搜索結果指出,從 reduce 的語義上來說,傳入閉包的參數(如果可變的話,即 mutated),會對底層序列的每個元素都產生一份 copy 。在我們的案例中,這意味着 accumulator 參數 ac 將為 0…100000 范圍內的每個元素都執行一次復制操作。有關對此更好、更詳細的解釋請看這篇 Airspeedvelocity 博客文章。

因此,當我們試圖使用 reduce 來替換掉一組操作時,請時刻保持清醒,問問自己:reduction 在問題中的情形下是否確實是最合適的方式。

現在,可以回到我們的初始問題:計算人口總數和平均年齡。請試着用 reduce 來解決吧。

再一次嘗試來寫 infoFromState 函數

  1.  
    func infoFromState(state state: String, persons: [[String: AnyObject]])
  2.  
    -> (count: Int, age: Float) {
  3.  
     
  4.  
    // 在函數內定義別名讓函數更加簡潔
  5.  
    typealias Acc = (count: Int, age: Float)
  6.  
     
  7.  
    // reduce 結果暫存為臨時的變量
  8.  
    let u = persons.reduce((count: 0, age: 0.0)) {
  9.  
    (ac: Acc, p) -> Acc in
  10.  
     
  11.  
    // 獲取地區和年齡
  12.  
    guard let personState = p[ "city"],
  13.  
    personAge = p[ "age"]
  14.  
    // 確保選出來的是來自正確的洲
  15.  
    where personState.hasSuffix(state)
  16.  
     
  17.  
     
  18.  
    // 如果缺失年齡或者地區,又或者上者比較結果不等,返回
  19.  
    else { return ac }
  20.  
     
  21.  
    // 最終累加計算人數和年齡
  22.  
    return (count: ac.count + 1, age: ac.age + Float(personAge))
  23.  
    }
  24.  
     
  25.  
    // 我們的結果就是上面的人數和除以人數后的平均年齡
  26.  
    return (age: u.age / Float(u.count), count: u.count)
  27.  
    }
  28.  
    print(infoFromState(state: "CA", persons: persons))
  29.  
    // 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:求兩個數的最小值
// min 閉包傳入兩個參數:1. 初始值 2. 遍歷列表時的當前元素
// 倘若當前元素小於初始值,初始值就會替換成當前元素
// 示意寫法: initial = min(initial, elem)
[1, 5, 2, 9, 4].reduce(Int.max, combine: min)

Unique

剔除列表中重復的元素。當然,最好的解決方式是使用集合(Set)

[1, 2, 5, 1, 7].reduce([], combine: { (a: [Int], b: Int) -> [Int] in
if a.contains(b) {
return a
} else {
return a + [b]
}
})
// prints: 1, 2, 5, 7

Group By

遍歷整個列表,通過一個鑒別函數對列表中元素進行分組,將分組后的列表作為結果值返回。問題中的鑒別函數返回值類型需要遵循 Hashable 協議,這樣我們才能擁有不同的鍵值。此外保留元素的排序,而組內元素排序則不一定被保留下來。

func groupby<T, H: Hashable>(items: [T], f: (T) -> H) -> [H: [T]] {
return items.reduce([:], combine: { (var ac: [H: [T]], o: T) -> [H: [T]] in
// o 為遍歷序列的當前元素
let h = f(o) // 通過 f 函數得到 o 對應的鍵值
if var c = ac[h] { // 說明 o 對應的鍵值已經存在,只需要更新鍵值對應的數組元素即可
c.append(o)
ac.updateValue(c, forKey: h)
} else { // 說明 o 對應的鍵值不存在,需要為字典新增一個鍵值,對應值為 [o]
ac.updateValue([o], forKey: h)
}
return ac
})
}
print(groupby([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], f: { $0 % 3 }))
// prints: [2: [2, 5, 8, 11], 0: [3, 6, 9, 12], 1: [1, 4, 7, 10]]
print(groupby(["Carl", "Cozy", "Bethlehem", "Belem", "Brand", "Zara"], f: { $0.characters.first! }))
// prints: ["C" : ["Carl" , "Cozy"] , "B" : ["Bethlehem" , "Belem" , "Brand"] , "Z" : ["Zara"]]

Interpose

函數給定一個 items 數組,每隔 count 個元素插入 element 元素,返回結果值。下面的實現確保了 element 僅在中間插入,而不會添加到數組尾部。

func interpose<T>(items: [T], element: T, count: Int = 1) -> [T] {
// cur 為當前遍歷元素的索引值 cnt 為計數器,當值等於 count 時又重新置 1
typealias Acc = (ac: [T], cur: Int, cnt: Int)
return items.reduce((ac: [], cur: 0, cnt: 1), combine: { (a: Acc, o: T) -> Acc in
switch a {
// 此時遍歷的當前元素為序列中的最后一個元素
case let (ac, cur, _) where (cur+1) == items.count: return (ac + [o], 0, 0)
// 滿足插入條件
case let (ac, cur, c) where c == count:
return (ac + [o, element], cur + 1, 1)
// 執行下一步
case let (ac, cur, c):
return (ac + [o], cur + 1, c + 1)
}
}).ac
}
print(interpose([1, 2, 3, 4, 5], element: 9))
// : [1, 9, 2, 9, 3, 9, 4, 9, 5]
print(interpose([1, 2, 3, 4, 5], element: 9, count: 2))
// : [1, 2, 9, 3, 4, 9, 5]

Interdig

該函數允許你有選擇從兩個序列中挑選元素合並成為一個新序列返回。

func interdig<T>(list1: [T], list2: [T]) -> [T] {
// Zip2Sequence 返回 [(list1, list2)] 是一個數組,類型為元組
// 也就解釋了為什么 combinator 閉包的類型是 (ac: [T], o: (T, T)) -> [T]
return Zip2Sequence(list1, list2).reduce([], combine: { (ac: [T], o: (T, T)) -> [T] in
return ac + [o.0, o.1]
})
}
print(interdig([1, 3, 5], list2: [2, 4, 6]))
// : [1, 2, 3, 4, 5, 6]

Chunk

該函數返回原數組分解成長度為 n 后的多個數組:

func chunk<T>(list: [T], length: Int) -> [[T]] {
typealias Acc = (stack: [[T]], cur: [T], cnt: Int)
let l = list.reduce((stack: [], cur: [], cnt: 0), combine: { (ac: Acc, o: T) -> Acc in
if ac.cnt == length {
return (stack: ac.stack + [ac.cur], cur: [o], cnt: 1)
} else {
return (stack: ac.stack, cur: ac.cur + [o], cnt: ac.cnt + 1)
}
})
return l.stack + [l.cur]
}
print(chunk([1, 2, 3, 4, 5, 6, 7], length: 2))
// : [[1, 2], [3, 4], [5, 6], [7]]

函數中使用一個更為復雜的 accumulator,包含了 stack、current list 以及 count 。

譯者注:有關 Reduce 底層實現,請看這篇文章

 

原文 http://swift.gg/2015/12/10/reduce-all-the-things/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM