Learning various programming paradigms is very useful to extend your mind and also design approaches in your main language. For example, this post describes a simple example where infinite lists, as in Haskell, allow us to solve a problem in swift more elegantly.
The simple task
Let’s say we have some basic data structure in our domain model:
1 2 3 4 5 6 7 8 9 10 11
We have a list of
Labels, which can be of any length, and we need to add a color to every
Label, create a new
ColoredLabel and send it to another system. We have a predefined set of colors, let’s say only three of them:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
I’ve also overridden the
debugDescription for our set of colors to make the debug output below more readable.
How do you color each label in case when we have more labels than colors, given that the colors should be looped?
The first solution could be simple, like this:
1 2 3 4 5 6 7 8 9 10
Unfortunately, it outputs only three colored labels instead of seven because
zip stops when either of the sequences runs out of elements.
zipping with manually extended colors
We can manually extend the number of available colors to have the correct number of them to
zip all our labels. If we ignore the constraint in our task for now (“the colors should be looped”), we could append the same color as many times as we need. How many times? For simplicity, we could append as many as there are labels:
1 2 3 4 5 6 7 8 9 10 11 12 13
Yes, it works, but we’re appending more elements than necessary. We can calculate the right number of course:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
Good, isn’t it? Not so good if we have more colors than labels:
1 2 3
Oh, right. Let’s make sure we don’t create arrays of negative length:
1 2 3 4 5 6 7 8 9 10 11 12 13
That works in all cases now, but with a bunch of boilerplate code. Trying to incorporate the colors looping here would create even more code and more complex logic.
Probably a more
swift-like (imperative) approach would be with a
for loop. Here’s how it could be implemented:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
Sure, this works and the color looping is easy to implement. However, I don’t like this solution for multiple reasons:
- It uses mutable variables, so the reader has to keep track of their state and know when and how they change.
- It has a lot (relative to the size of our task) of boilerplate syntax: declaring the variables and manually updating them.
- The code is unsafe and it is very easy to get a runtime exception if you use the wrong index somewhere, e.g.
let color = defaultColors[colorIndex % (defaultColors.count + 1)]— relatively easy to spot in this tiny example, but harder in some real code.
- And finally, it violates the Single Responsibility Principle, we’re mixing up multiple concerns here: WHAT to do — creating a
ColoredLabeland HOW to do it — managing the color index, calculating the current color, appending the new colored label to an array.
Again, what we want is a list of looped colors, as many of them as necessary. However instead of creating the list beforehand (“push” style), we could use a list, which we could ask to get the next element, and the next, and the next, and so on (“pull” style). So we really need just an infinite list. In Haskell there is a function called
cycle that cycles a finite list infinitely, it would be great to have it in swift:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
cycle implementation is abstracted, we don’t care how it works as long as it does what it should do. The solution to our task is very simple now:
1 2 3
We’ve separated the concerns here: what to do — creating a
ColoredLabel in the lambda, and the data to use —
Don’t forget that you can’t iterate over the whole infinite list, you must have a terminating condition, for example by using
cycle on an empty array?
One note: this
cycle implementation will crash if called on an empty array. Well, calling it that way doesn’t make any sense, what would it do?
- It could return an empty sequence, but that would violate the expected behavior.
- We can add a precondition with a meaningful message, but that’s still a runtime error.
- The best solution is to follow the types and implement it only for non-empty arrays. Unfortunately, that type is not in the swift’s standard library, and you can find an implementation online — this guarantees type safety at compile time, no more runtime errors!
I believe this solution using
zip is very neat and elegant, and is actually easier to understand than the others in this post. Yes, there is a small barrier to understanding infinite lists if you’ve never seen them, but once you do, they become obvious to you.
Even in this trivial task the solution with infinite lists is simpler than others. If you recognize similar problems and can apply the same approach to bigger tasks, you’ll have even bigger benefit in the clarity of your code.