Journey from loops to Functors

Marek Galik
7 min readNov 19, 2022

--

Autumn sunset

We’ve all been there. Including me. The second thing that I learned right after variables were loops. So join me in my journey back in time and see how we can improve something as simple as a function that calculates the square roots of numbers in an array.

I started with while loop. And I can still remember how breathtaking it was for me.

func squaresWhile(of numbers: [Double]) -> [Double] {
var index = 0
var squareRoots = [Double]()
while index < numbers.count {
squareRoots.append(sqrt(numbers[index]))
index += 1
}

return squareRoots
}

Then I tried do-while. Back then it was still Objective-C and in Swift it's now called repeat-while.

func squaresRepeat(of numbers: [Double]) -> [Double] {
var index = 0
var squareRoots = [Double]()
repeat {
squareRoots.append(sqrt(numbers[index]))
index += 1
} while index < numbers.count

return squareRoots
}

Right away we can see that these versions are quite robust and we need to use two variables, one for index, the second for new values, and yet we are doing a very simple operation.

The next and last loop for me was the for loop.

func squaresFor(of numbers: [Double]) -> [Double] {
var squareRoots = [Double]()
for num in numbers {
squareRoots.append(sqrt(num))
}

return squareRoots
}

The “improvement” pattern was quite obvious. It seemed to me incredible that we no longer need to hold the current index. For a short period time, I believed that this is it and it can’t get any better.

But then, when I was getting deeper and deeper into the Swift language I discovered this weird function called map:

func squaresMap(of numbers: [Double]) -> [Double] {
numbers.map(sqrt)
}

Suddenly, the same logic has instead of seven lines three. But are there any benefits compared to standard loops?

  • There are no variables, just constants. That means that we no longer need to manage the local state of variables.
  • As we can see, it’s much more difficult to introduce common bugs like index overflow or skipped value. The number of items in the input array needs to correspond to the number of items in the output array. Something very trivial and yet something that cannot be guaranteed by simple loops.
  • It has become almost impossible to produce side effects with this functional approach.
  • You don’t need to wrap this kind of simple logic into a function because the logic inside of it is simple and self-explanatory.

But still… Do we really need to declare the name of the input just to use it once? Again we can reduce the code even further to the point-free version:

let squareArrayTacit = (Array<Double>.map |> flip)(sqrt)

The only trick is the flip function. And the reason is, that the first argument of the map function is the array and the second is the transformation function, so we need to flip these, so we can first provide the function and then the array. And if you think about it, it's pretty bizarre that we usually need to give names to something that we don't even use.

So in the end we see that we were able to shrink our logic from the initial 9 lines to just one. Not only that, but we were able to get rid of all variables and constants and we just needed one kind of thing — functions.

Now with Optional

To better understand the beauty of map, let's try it one more time with an optional number. So, let's try to write a function that calculates the square root of an Optional<Double>:

func optionalSquareGuard(of number: Double?) -> Double? {
guard let number else { return nil }
return sqrt(number)
}

And this seems fine right? Let’s try it one more time with map:

func optionalSquareMap(of number: Double?) -> Double? {
number.map(sqrt)
}

And a point-free version:

let squareOptionalTacit = (Optional<Double>.map |> flip)(sqrt)

Now let’s pause for a second. Isn’t that strange? The solution looks almost the same as the solution for the array. Let’s compare these side by side:

let squareArrayTacit = (Array<Double>.map |> flip)(sqrt)
let squareOptionalTacit = (Optional<Double>.map |> flip)(sqrt)

So the only difference between these two is the type? For many people this might be strange, others might think it’s fascinating. But what’s the reason behind it? Truth is that like many other types, both Array and Optional are so-called functors.

Magic of functors

The easiest way how to explain what is happening is that every type for which is map a defined function is a functor. There are many functors in Swift and what’s interesting is that they don’t need to be collections. The main purpose of functors is to wrap a value in order to give it additional context. In other words, the functor is generic over the type that it “wraps”. One post had a good metaphor with a box to explain functors.

         .+------+               .+------+    
.' | .'| .' | .'|
+---+--+' | MAP +---+--+' |
| 123 | | ---> | ABC | |
| ,+--+---+ | ,+--+---+
|.' | .' |.' | .'
+------+' +------+'

This box can contain all kinds of data. And it doesn’t really matter what kind of context it adds to that data. But regardless of what is in the functor and for what reason, you can use the function map to modify the content. There are two types of transformation. The transformation that keeps the type of the wrapped value is called isomorphic. On the other hand, a transformation that changes the type of the wrapped value is called polymorphic. Polymorphic transformation is significant because it changes the generic type of the corresponding functor.

Swift is really a powerful language and an Array, Set and Optional are not the only functors in the language. That leads us to so-called bi-functors.

Magic continues with Bi-functors

You already know that functor is generic over a single type. You’re guessing correctly, that the prefix bi stands for generic over two types. It's that simple. The simplest example of a bi-functor is a well-known result type, which is generic over a Success value and Failure. Let's try our example with this type:

let sqrtResultTacit = (Result<Double, Never>.map |> flip)(sqrt)

Again we see, that we have the same function, only the namespace is different. The number of map functions corresponds to the number of generic types. In the case of the Result type, there is also a mapError<NewFailure>(_:) function.

The other famous category of bi-functors widely used in Swift are various Publishers in the Combine framework. Without going into any details you can clearly see that it's again similar to the Result type:

let sqrtPublisherTacit =
(AnyPublisher<Double, Never>.map |> flip)(sqrt)
let sqrtCurrentTacit =
(CurrentValueSubject<Double, Never>.map |> flip)(sqrt)
let sqrtPassTacit =
(PassthroughSubject<Double, Never>.map |> flip)(sqrt)

So far we’ve started with a simple loop and now we have several functors serving different purposes and yet the map function works pretty much in the same way.

Missing bi-functor

Have you ever wondered, why the second type of the Result needs to be always an Error? In my previous post, I mentioned that Swift lacks one of the simplest sum type called Either which is also a Functor. So let's create one.

public enum Either<Left, Right> {
case left(Left)
case right(Right)
}

Our new bi-functor is as simple as this. Now we need simply need to implement our map functions:

public extension Either {
func leftMap<NewLeft>(
_ transform: (Left) -> NewLeft
) -> Either<NewLeft, Right> {
switch self {
case .left(let value):
return .left(transform(value))
case .right(let value):
return .right(value)
}
}

func rightMap<NewRight>(
_ transform: (Right) -> NewRight
) -> Either<Left, NewRight> {
switch self {
case .left(let value):
return .left(value)
case .right(let value):
return .right(transform(value))
}
}
}

You can read more about this functor here.

Just to confirm, we can write our square root map function as we did with other functors:

let sqrtEither = (Either<Double, Int>.leftMap |> flip)(sqrt)

Ubiquitous Functors

As you can see, map is not just another fancy function and I think that there's no doubt about its importance. In fact, this simple function is so important that it has its own operator which looks like this: <$>. We can write our implementation of this operator for every available functor. Thus, that would take ages. Thankfully there's a library for that called Runes. Unfortunately, Swift doesn't allow us to create an operator that includes the dollar sign, so the company thoughtbot used an alternative version that looks like: <^>. As before, let's see a simple example with this operator:

sqrt <^> [4, 9, 16]

Thanks to this operator you can also create pipeline-like code and for many people, this is a preferable approach to tacit code.

This library also contains other operators but I’ll get to those in the future.

What’s next

Since my personal “discovery” of Functors it’s almost impossible to go back. And yet there’s a catch with functors. What if the lambda in the map function returns the same functor? Let’s have an example:

let intFromString = (Optional<String>.map |> flip)(Int.init)

The return of this function is Int?? and that's probably something we'd like to avoid. And to do that we need something more powerful than functor. But that's for another time. See you soon.

--

--