Swift Iteration Showdown

I've had conversations where people don't see the benefit of using functions like map over hand rolling a for loop with an accumulator, with the main argument being that everyone understands a for loop so "why add complexity?". Whilst I agree that most developers could rattle off a for loop in their sleep it doesn't mean we should use the lower level constructs that have more risk. I compare it to a light switch in my house, I fully understand how to connect two wires to turn a light on but I'm not about to enter a dark room and start twiddling with wires when I can just use a switch.

The other trend I have picked up on during code reviews and discussions is that people are happy to use higher order functions like map, forEach and reduce but because they've not come from a functional programming background they can often misuse these functions. To make the most of these functions and to avoid surprises for future you and your team mates there are some rules that should be obeyed (some functional languages enforce these rules). The misuse I see is generally around putting side effects where they don't belong.

I think the best way to get people on board is to start off with examples of basic iteration and progressively change the iteration style whilst analysing the potential areas for improvement with each code listing. Hopefully by the end we'll have a shared understanding of what is good/bad and be able to identify the tradeoffs in each iteration style.


Basic iteration

The most basic iteration in most languages is some variation of for (int i = 0; i < end; i++) { ... }. This is not available in Swift but we can approximate it with the following

let collection = [ 1, 2, 3, nil, 5, 6 ]

var index = 0
while index < collection.count {
  print(collection[index])
  index += 1  
}

There are a lot of things about this loop that could be considered bad:

Whilst this iteration works you can see from the list above that there is plenty of room for improvement for making this safer and reducing complexity.


Safer indexing

We can make an improvement on the above by getting the indices from the collection itself

let collection = [ 1, 2, 3, nil, 5, 6 ]

for index in collection.indices {
  print(collection[index])
}

The improvements here are quite nice:


Remove indexing

Most of my complaints so far have been related to indexing - both calculating the correct index and then performing the access. We can improve this situation by using for/in syntax.

let collection = [ 1, 2, 3, nil, 5, 6 ]

for item in collection {
  print(item)
}

The improvements here are:

The previous disadvantages are now mostly gone. To continue evaluating the tradeoffs we need to step our thinking up a level to see the improvements that can be made. Some bad things about the above are:


forEach

To handle all the issues raised so far we can jump to using forEach

let collection = [ 1, 2, 3, nil, 5, 6 ]

collection.forEach { print($0) }

There are a few nice advantages with this implementation:

  func forEach(_ body: (Element) throws -> Void) rethrows
  

you can see that there is no return value - this is a big hint that this function is all about side effects. If a function does not return a result then for it to add any value it needs to cause side effects. For clarification in all the examples above the side effect has been printing to stdout.


map

We get the same benefits (listed above) when we use map. The difference here is that the function signature reveals a different intent:

func map<T>(_ transform: (Element) throws -> T) rethrows -> [T]

This function does have a return type, which suggests this function is all about creating something and not having side effects. The function signature also shows that we need to provide a function that transforms Element to T, which provides the full picture - this function will create a new collection by using the provided function to map values. In terms of inferring that this function should not have side effects you can derive this from a few different principles:

For comparison here's classic iteration vs map:

let collection = [ 1, 2, 3, nil, 5, 6 ]

// Classic
var results = [Int?]()
for item in collection {
  results.append("\($0)")
}
print(results)

// map
print(collection.map { "\($0)" })

You can see that classic iteration introduces a few issues again:


Wrap up

The issues I have seen in code reviews are that people are happy to use forEach, map and other higher order functions but they then muddy the water by not following the intent of the functions and introducing side effects in functions that should be side effect free or adding an accumulator to a function that should be purely about side effects. I've even seen multiple scenarios where people use a map and also have an accumulator for collecting different bits, there is almost always a more appropriate API to prevent us from the misuse.

In order to take advantage of the higher level functions we should make sure that we follow the rules around side effects and single responsibility. The aim should be to use these functions to clarify intent, the intent becomes confusing if you use a map and also cause side effects or if you use forEach whilst modifying an accumulator. If we stick to the rules then it makes the task of reading code simpler as the functions behave in known ways and we don't need to become experts at recognising patterns.

This post is in no way advocating that you go and change every iteration to use higher order functions but I do hope that you'll be able to know the tradeoffs and decide when it is appropriate to use each tool. My real aim with this post is to allow people who have read it to have a deeper appreciation of the tradeoffs around different iteration techniques and to allow for a shared base knowledge to build ideas upon.