Explore the origins of the reduce function

Today we joined the Utrecht University Assistant Professor Wouter Swierstra, he is Functional Swift's coauthor. One area of his work is functional programming, he was pleased to see some of the ideas from this area, it has been working for a long period of time, such as Swift is becoming the dominant language.

We will discuss functional programming rabbit hole in a few concentrated. More specifically, we will focus on reduce. To remind yourself what reduce, let's write some examples.

Reduction of examples

We create an array to hold the numbers from 1 to 10, we call reduceit to find the maximum number of array. The function takes two parameters: the initial value of the result, the individual array elements and function in combination with the results together. As we passed a small Intinitial value, we maxused a combination of functions:

let numbers = Array(1...10)
numbers.reduce(Int.min, max) // 10
复制代码

We can also reduceby passing zero and the +operators to calculate the sum of all the elements:

numbers.reduce(0, +) // 55
复制代码

Let's take a closer look reduceon the function signature Array:

func reduce<Result>(_ initialResult: Result, _ nextPartialResult: (Result, Self.Element) throws -> Result) rethrows -> Result
复制代码

The function in which Resultthe type is generic. In the above two examples, the results and the type of the array element Inttypes are, but not necessarily match types. For example, we can also reducebe used to determine whether the array contains elements. The reduceresults of the phone is 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
复制代码

We call the reduceinitial results false, because if the array is empty, it must be the result. In the combined function, we check the incoming element is equal to the elements we are looking for, or the results so far are equal true.

This version containsis not the most efficient , because it does more work than it needs to. However, finding a use is to achieve an interesting exercise reduce.

List

But reducecome from? We can define a single list and reduceto explore its origins lookup operation on it.

In Swift, we will be defined as the enumeration list, in which case sensitive and non-empty list containing an empty list. Conventionally known as a non-empty cases cons, which is a single list element associated value and a tail. The tail is another list, which allows recursive case, we must be marked as indirect:

enum List<Element> {
    case empty
    indirect case cons(Element, List)
}
复制代码

We can create a list of integers, as follows:

let list: List<Int> = .cons(1, .cons(2, .const(3, .empty)))
复制代码

Then we define a function called fold, looks like reduce, but it's a little different:

extension List {
    func fold<Result>(_ emptyCase: Result, _ consCase: (Element, Result) -> Result) -> Result {

    }
}
复制代码

These two arguments foldand two cases match is not accidental List. In the implementation of a function, we use each parameter and its corresponding case:

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))
        }
    }
}
复制代码

Now we can foldcalculate the sum of its elements in the list:

list.fold(0, +) // 6
复制代码

We can also foldbe used to find the length of the list:

list.fold(0, { _, result in result + 1 }) // 3
复制代码

In demonstration foldthere is correspondence between and declarations List.

We can enum cases Listdeemed to construct a list of two ways: one is to construct an empty list, and the other is to construct a non-empty list.

And foldthere are two parameters: one for the .emptycase, for a .conscase - what information we need in order to calculate the results of each case.

If we think that emptyCaseargument is not the type of value Result, but as a function () -> Result, and then .emptybecome clearer correspondence between the constructor.

Folding and reduced

The foldfunction is almost the same reduce, but with one small difference. We can prove the difference between the two by calling the two functions and compare the results.

First, we call fold, transfer the constructor of the two cases Listas an argument:

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
*/
复制代码

We see exactly the same results as the original list. In other words, foldusing two case constructor calls a method for the preparation of identity is a complex function: no change.

Then we have reducean array, the same incoming constructors List- except we must exchange consthe order parameter case, since the first reducepass and the second pass the integration result of the current element:

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
*/
复制代码

When we checked this reducetime the result of the call, we see it Listin the opposite order contains array elements, because reducethrough the array and each element is processed into results. This is what is different fold, because it left to right through the list, and only emptyCasewhen it reaches the very end of the list when using this value.

There are many operations, such as calculating the sum or length, reduceand foldgives the same results. However, by order of important operations, we begin to see differences in behavior of the two functions.

List.reduce

We have achieved fold, we have used the Swift  Array.reduce, but look at its implementation is also very interesting List.reduce. We write the function in the extension, and give them the same parameters fold:

extension List {
    func reduce<Result>(_ emptyCase: Result, _ consCase: (Element, Result) -> Result) -> Result {
        // ...
    }
}
复制代码

In order to achieve this, we will emptyCaseassign parameters to the initial results, and then we switch the list to see if it is empty. If it is empty, we can return the results immediately. If the list is non-empty, we will xadd elements that we used to date consCasethe results of the function to see, and we recursive call reducethe tail, the result of cumulative transfer:

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)
        }
    }
}
复制代码

Tail recursion

Here we can see that it reduceis tail-recursive: it will either return a result, either immediately recursive call. foldTail recursion is not, because it calls consCasethe function, and more or less hidden recursion and used to construct the function of the second parameter.

This difference leads to different results, we can now see more clearly through a comparison of two methods 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)))`
复制代码

Using tail-recursive operation can be easily rewritten with a cycle:

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
    }
}
复制代码

This version of reduce1the results generated by reduce:

list.reduce1(List.empty, List.cons) // .cons(3, .cons(2, .cons(1, .empty)))
复制代码

reduceFolding operation is just one example, we can define these operations actually in many other structures.


Original Address: talk.objc.io/episodes/S0...

Guess you like

Origin blog.csdn.net/weixin_34319374/article/details/91398000