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 reduce
it 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 Int
initial value, we max
used a combination of functions:
let numbers = Array(1...10)
numbers.reduce(Int.min, max) // 10
复制代码
We can also reduce
by passing zero and the +
operators to calculate the sum of all the elements:
numbers.reduce(0, +) // 55
复制代码
Let's take a closer look reduce
on the function signature Array
:
func reduce<Result>(_ initialResult: Result, _ nextPartialResult: (Result, Self.Element) throws -> Result) rethrows -> Result
复制代码
The function in which Result
the type is generic. In the above two examples, the results and the type of the array element Int
types are, but not necessarily match types. For example, we can also reduce
be used to determine whether the array contains elements. The reduce
results 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 reduce
initial 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 contains
is 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 reduce
come from? We can define a single list and reduce
to 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 fold
and 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 fold
calculate the sum of its elements in the list:
list.fold(0, +) // 6
复制代码
We can also fold
be used to find the length of the list:
list.fold(0, { _, result in result + 1 }) // 3
复制代码
In demonstration fold
there is correspondence between and declarations List
.
We can enum cases List
deemed 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 fold
there are two parameters: one for the .empty
case, for a .cons
case - what information we need in order to calculate the results of each case.
If we think that emptyCase
argument is not the type of value Result
, but as a function () -> Result
, and then .empty
become clearer correspondence between the constructor.
Folding and reduced
The fold
function 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 List
as 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, fold
using two case constructor calls a method for the preparation of identity is a complex function: no change.
Then we have reduce
an array, the same incoming constructors List
- except we must exchange cons
the order parameter case, since the first reduce
pass 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 reduce
time the result of the call, we see it List
in the opposite order contains array elements, because reduce
through 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 emptyCase
when it reaches the very end of the list when using this value.
There are many operations, such as calculating the sum or length, reduce
and fold
gives 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 emptyCase
assign 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 x
add elements that we used to date consCase
the results of the function to see, and we recursive call reduce
the 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 reduce
is tail-recursive: it will either return a result, either immediately recursive call. fold
Tail recursion is not, because it calls consCase
the 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 reduce1
the results generated by reduce
:
list.reduce1(List.empty, List.cons) // .cons(3, .cons(2, .cons(1, .empty)))
复制代码
reduce
Folding operation is just one example, we can define these operations actually in many other structures.