Mapping non-nil elements in Swift Array
Nobody said it was easy, no one ever said it would be so hard.
So, let’s say we have an array of optional elements, and we want to do two things:
- Remove all elements that are
nil
; - Map all elements that are not
nil
;
There are a few ways to do that in Swift, and I’m gonna walk us through them, and why I decided to make my approach.
Example problem case:
We have a function that we want to apply to all numbers of an array of Optionals, resulting in an array with only the resulting mapped numbers:
1. Plain old for loop:
Probably the first solution that comes to mind is our old friend, the for loop. And that might look something like this:
It works fine, but we have to make doubledNumbers
a mutable variable and use the forced unwrapping which I’ve never been a fan.
With the for loop approach we can’t get rid of mutability, but we can avoid the forced unwrapping:
We traded mutability for an increase in the cognitive load by using the continue
instruction, and also, this mutable array is not going to let me sleep at night, so let’s try and find another solution.
2. Filtering and mapping:
Let’s turn to our functional friends: filter
and map
. So we can get rid of those pesky nils and keep our data structures immutable:
We now achieved immutability but reintroduced the forced unwrapping on the map
function, since we can’t skip iterations using the continue
instruction as done before.
Fortunately, Swift is our friend, and it gives us a function that looks promising: compactMap(_:)
.
3. Compact mapping
The Swift docs describe compactMap
as:
Returns an array containing the non-
nil
results of calling the given transformation with each element of this sequence.
In other words, it applies a function to each element of the array, and it drops the nil values, precisely what we set out to accomplish. But if we try passing our double
function to compactMap
then the compiler is not going to be too happy with us:
The reason we can't do this is because double
is of type (Int) -> Int
but compactMap
expects a function that can handle the Optionals in our numbersWithNils
array and that might also return nil
which means, in our case, that the function needs to be (Int?) -> Int?
.
I don't feel comfortable changing the double
function since there is no reason for it to receive accept an optional parameter other than the one we're facing currently. So this is the best thing we can do with compactMap
for now:
This seems like a good solution since there is no more mutability or forced unwrapping. But I still wasn't satisfied.
Applying a function (that does not accept Optionals) to an array of Optionals, removing the nil
values seem like a necessity that might be reoccurring, so in order to eliminate the boilerplate of the solution above, I asked friends for help, and we found a way to generalize it.
4. Convenience method (our approach)
Besides the functionality we already have, the goal now is to generalize it to work with any array of Optionals, and with no boilerplate. So let's add a method to the Array
class, via an extension:
The new mapNonNils
function works for every array, assuming that it's elements are of type Optional<E>
and that the transform
parameter maps an element of type E
to another of type T
. BeingE
and T
any types (generics FTW!).
So now we can solve our initial problem in the following way:
Let's evaluate this solution:
- Removes all elements that are
nil
; - Maps all elements that are not
nil
; - No mutability;
- No forced unwrapping;
- Single loop;
- Works for every type and function;
- No boilerplate required;
Community improvements
The solutions and improvements bellow came from the community, in order to build an even better solution. This list will be updated as I receive more feedback.
1. Daniel Oliveira's Optional map:
The first improvement came while reviewing the draft of this article. Daniel astutely pointed out that Swift provides a map
function for Optionals. This map applies the transform function only if the Optional instance is not nil
, otherwise, it returns nil
.
This not very well known feature goes really well with compactMap
, making our code more concise:
Some might even argue that this makes removes the need for the extension, since now the boilerplate is very small. But for me, it's still boilerplate and providing a method that abstracts all boilerplate gives a more convenient API for developers. Calling compactMap
with another map
as a transform function does not feel very intuitive to me, but I'd love to hear your opinions: do you think mapNonNils
is no longer necessary?
Conclusion
The solution we’ve come up with seems pretty good, but I do believe there is room for improvement. The other reason for this article is to enlist the community into fiding those improvement opportunities, so I look forward to seeing what you guys come up with on the commentaries and update the article accordingly.
Thanks!