/ swift

Functional Refactoring in Swift

Swift lends itself well to functional programming, but Swift is not necessarily a functional language. We are free to choose the style of programming that best matches our style and requirements.

Writing Swift code in a functional way isn't just en vogue, it has benefits:

  • You can take advantage of immutability, which offers less side effects and advantages for parallelization
  • You can express what you want instead of how and allow the Swift compiler team make optimizations on how to make that operation blazing fast
  • You can avoid an entire class of mistakes, such as off-by-one errors and other things that can sometimes be too easy to make with imperative programming styles

This endeavor to make things functional isn't always inherently good, however. You have to make a judgement call.

Does this make the code more readable? More obviously correct? Faster? Easier to test?

While we might be tempted to go off the deep end and rewrite everything in a functional style, it's easy to go too far.

Here's an example I came across recently where I wanted to clean something up and explore a functional approach to the same algorithm.

Take this view, which renders a few randomized points along its y-axis:

Say we want to take these points and average them. To do that we will need to take points 0 and 1, add their x values and y values together, and divide by two.

An imperative approach to this problem might look something like this:

var averagedPoints: [CGPoint] = []
for i in 0..<points.count-1 {
    let p1 = points[i]
    let p2 = points[i+1]
    let a = CGPoint(
        x: (p1.x + p2.x) / 2,
        y: (p1.y + p2.y) / 2
    )
    averagedPoints.append(a)
}

This code creates a temporary mutable array to store our averaged points. Then we iterate over the indices in the array, except for the last one. We do this because each iteration of this loop needs to take the current point and the next one. We need to be careful here not to walk off the end of the array.

We then add our averaged point and draw the new line, which looks like this:

This code works, but let's stop and examine it for a minute.

  • It carries around a temporary mutable variable. The averagedPoints array isn't useful beyond this loop, and yet we have to have this mutable variable around so we can append to it. If we wanted to take this work and split it across a number of threads, we would have to be careful to add locks around modifying the array.
  • It has numerous opportunities for off-by-one errors. If we use the wrong kind of range (...) or we forget to subtract 1 from the count, we will walk off the end of the array and crash.

So while this code works, it's not perfect and could perhaps be improved by programming in a functional style.

So what might a functional approach look like?

My first attempt at this was using reduce. After all, we are reducing the number of points by 1 here (so map doesn't fit, since it's 1:1).

The hard part here is that we need the previous point along with the current point in order to compute the average. This led me to create a tuple as my initialResult parameter.

let initial: ([CGPoint], CGPoint) = ([], points.first!)

I defined this variable separately so we could easily see the type annotation and to make the reduce call less daunting:

let result = points.dropFirst().reduce(initial) { partial, point in
    let averagedPoints = partial.0
    let previousPoint = partial.1
    let a = CGPoint(
        x: (previousPoint.x + point.x) / 2,
        y: (previousPoint.y + point.y) / 2
    )
    return (
        averagedPoints + [a],
        point
    )
}
let averagedPoints = result.0

If you're not familiar with reduce, this may look a little scary. Each time in the block we're passed our partial result, which is the tuple defined in the initial variable declaration. We need to return one of these each time through the loop.

Also, note the use of dropFirst() here, which is a nice way of iterating over the rest of the points, since we are seeding the function with the first point manually.

By passing along our current point in the tuple, the next iteration will get it as the previous point, allowing us to calculate the average.

This code works, but is it an improvement?

  • We have this annoying tuple to work with. We have to immediately unpack this and give a name to each part, to avoid confusion. We also have to unpack the final result here, since we don't care about the previous point once we're finished
  • It's more lines of code. Visually this is harder to understand what we're doing, and it's a lot more complicated to follow the flow.

Overall, this is a bad change. It is functional, but is a loss for readability in my opinion.

Can we do better?

Let's see if we can do better. The reduce version was complicated because we had to carry along this previous value each time through the loop.

What if we could iterate over each pair in the way we want, where the loop is passed both points?

zip is perfect for this!

When you zip two sequences, you get back pairs of values, one from each sequence. The shorter sequence defines the length of the resulting zipped result (in other words, you can't get any nils).

If we zip our array of points with itself (offset by one) then we'll get this coalescing of values that we can map over:

let pairs = zip(points, points.dropFirst())
let averagedPoints = pairs.map { pair in
    return CGPoint(
        x: (pair.0.x + pair.1.x) / 2,
        y: (pair.0.y + pair.1.y) / 2
    )
}

This code is a lot better than the reduce version. It has the functional benefits we outlined above and it happens to be shorter than the imperative version too.

Is it more readable? This is debatable. I think it is, but that's because I understand and recognize the zip function. For someone who isn't familiar with this, it might be confusing.

I think this is a good reason why it is important to learn and practice these functions, as they can give you building blocks you can use to transform values in your system using a language that everyone understands.

As always, make sure and understand the tradeoffs that you're making here. Don't sacrifice readability and performance for the sake of being functional. But when you find situations that could be expressed at a higher level, knowing these common functions is incredibly helpful at taking complex operations and expressing them in a functional way.