Map, FlatMap, Reduce & More

Map, compactMap & Filter

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 provided acc: [Int], the second is the current object from our sequence obj: 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.