Map
Lets re-implement map
and call it rmap
(short for reduce 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
})
}
let input = [1, 2, 3, 4]
let output = rmap(input, transform: { $0 * 2})
assert(output == [2, 4, 6, 8])
This is a good example to understand the basics of reduce
.
- First, we're calling reduce on a sequence of elements
elements.reduce...
. - Next, We're giving it the accumulator, i.e. an empty Int array,
which will form or return type / result
[Int]()
- After that, we're handing in the
combinator
which takes two parameters. The first is the accumulator which we just providedacc: [Int]
, the second is the current object from our sequenceobj: Int
. - The actual code in the
combinator
is simple. We simply transform the obj and append it onto the accumulator. We then return the accumulator.
This looks like much more code than just calling map
. That's indeed
true, but the version above is extra detailed in order to better explain
how reduce
works. We can simplify it.
func rmap(_ elements: [Int], transform: (Int) -> Int) -> [Int] {
return elements.reduce([Int](), combine: {$0 + [transform($1)]})
}
print(rmap([1, 2, 3, 4], transform: { $0 * 2}))
// [2, 4, 6, 8]
This still works fine. What happened here? We're using the convenient
fact that in Swift, the +
operator also works for two sequences. So
[0, 1, 2] + [transform(4)]
takes the left sequence and adds the right
sequence, consisting out of the transformed element, to it.
It should be noted that, as of right now, [1, 2, 3] + [4]
is slower
than [1, 2, 3].append(4)
. If you operate on huge lists, instead of
using collection + collection, you should have a mutable accumulator and
mutate it in place:
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
})
}
In order to better understand reduce
we will now go on and also
implement compactMap
and filter
.
func rcompactMap(_ elements: [Int], transform: (Int) -> Int?) -> [Int] {
return elements.reduce([Int](),
combine: { guard let m = transform($1) else { return $0 }
return $0 + [m]})
}
print(rcompactMap([1, 3, 4], transform: { guard $0 != 3 else { return nil }; return $0 * 2}))
// [2, 8]
The main difference is that we're adding a guard
to make sure the
optional contains a value.
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]
Again, a simple operation. We're leveraging guard again to make sure our filter condition holds.
Up until now, reduce
may feel like a more complicated version of map
or filter
without any major advantages. However, the combinator does
not need to be an array. It can be anything. This makes it easy for us
to implement various reduction operations in a very simple way.