探索reduce函數的起源

今天咱們加入了烏特勒支大學助理教授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
複製代碼

讓咱們仔細看看reduceon 的函數簽名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,由於若是數組爲空,這必須是結果。在組合函數中,咱們檢查傳入的元素是否等於咱們正在尋找的元素,或者到目前爲止的結果是否相等trueui

這個版本contains不是最高效的,由於它作的工做比它須要的多。然而,找到一個使用的實現是一個有趣的練習reducespa

名單

可是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- 除了咱們必須交換conscase 的參數的順序,由於首先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當它到達列表的最末端時才使用該值。

有不少操做,好比計算總和或長度,reducefold給出相同的結果。可是經過秩序重要的操做,咱們開始看到兩個函數的行爲差別。

List.reduce

咱們已經實現了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只是摺疊操做的一個例子,咱們實際上也能夠在許多其餘結構上定義這些操做。


原文地址:talk.objc.io/episodes/S0…

相關文章
相關標籤/搜索