探索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…

相关文章
相关标签/搜索