Swift 4.1 introduction of compactMap

Muhammad Shuaib Khan
Bumble Tech
Published in
4 min readApr 16, 2018

In functional programming, we have a clear definition for what a flatMap function is supposed to be. The flatMap method takes a list and a transformative function (which expects zero or more values for each transformation), applies the transformative function to each member of the provided list, and produces a single, flattened list. This is different to the simple map function — which applies the transformative function to each value and expects exactly one value for each transformation.

Swift has had map and flatMap for a few versions now. However in Swift 4.1 you can no longer use flatMap over a sequence with a transformation closure that returns an optional value. Instead, now we have a dedicated method for this specific case, named compactMap.

It might be a bit confusing at first to understand this change. If flatMap was working fine for this case, why introduce a separate method for it? Let’s try to work out what’s going on.

Swift standard library, prior to 4.1, provided three overloads for flatMap:

  1. Sequence.flatMap<S>(_: (Element) -> S) -> [S.Element] where S : Sequence
  2. Optional.flatMap<U>(_: (Wrapped) -> U?) -> U?
  3. Sequence.flatMap<U>(_: (Element) -> U?) -> [U]

Let’s go over each of these overloaded variants and look at what they do.

Sequence.flatMap<S>(_: (Element) -> S) -> [S.Element] where S : Sequence

The first overload is for a sequence where your transformation closure takes an element of this sequence, and provides another sequence. flatMap eventually flattens all of these transformed sequences into a final sequence, which is returned as the result. For example:

let array = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
let flattened = array.flatMap { $0 } // [1, 2, 3, 4, 5, 6, 7, 8, 9]

This is a perfect example of how flatMap is supposed to work. We map over each member of our original list, and produce a new sequence from it. flatMap is responsible for making sure our eventual result is a flattened structure of all of these transformed sequences.

Optional.flatMap<U>(_: (Wrapped) -> U?) -> U?

The second overload is for optionals. If the optional you call it on has a value, your transformation closure will be called with the unwrapped value, and you can return a transformed optional value.

let a: Int? = 2
let transformedA = a.flatMap { $0 * 2 } // 4
let b: Int? = nil
let transformedB = b.flatMap { $0 * 2 } // nil

Sequence.flatMap<U>(_: (Element) -> U?) -> [U]

The third overload is what we are interested in to understand the need for compactMap. This version looks similar to the first one, but it differs in one important way. In this case, your transformation closure is returning an optional. flatMap handles this by skipping any ‘nil’ return values, all other values will be included in the result as unwrapped values.

let array = [1, 2, 3, 4, nil, 5, 6, nil, 7]
let arrayWithoutNils = array.flatMap { $0 } // [1, 2, 3, 4, 5, 6, 7]

However, no flattening is performed in this case, thus this version of flatMap is closer to map than it is to the purely functional definition of flatMap. The problem with this overload is that it can be easily misused to work in places where map would work just fine.

let array = [1, 2, 3, 4, 5, 6]
let transformed = array.flatMap { $0 } // same as array.map { $0 }

The above usage of flatMap matches the third overload, implicitly wrapping the transformed value into an optional, and then unwrapping it to add to the result. The situation gets more interesting if the misuse is performed over a value transformation to strings.

struct Person {
let name: String
}
let people = [Person(name: “Foo”), Person(name: “Bar”)]
let names = array.flatMap { $0.name }

In versions of Swift prior to 4.0, you would get a transformation to [“Foo”, “Bar”]. But, since Swift 4.0, Strings now conform to the Collection protocol. Thus our usage of flatMap here, instead of matching the third overload, will match the first one and give you a “flattened” result of your transformed values: [“F”, “o”, “o”, “B”, “a”, “r”].

You will not see an error when you call flatMap because the usage is legal, but your logic is now broken because the result has a different type (Array<Character>.Type) from what you expected (Array<String>.Type).

Conclusion

Thus to avoid this misuse of flatMap, the third overloaded version has now been removed. Instead, to achieve this particular use case of it (i.e. to purge nil values), we now have a separate method to do just that.

--

--