Containing side-effects with the IO functor

Tyrone Michael Avnit
Jun 14 · 5 min read

I have been reading Professor Frisby’s Mostly adequate guide to Functional Programming. A great read, and a book I would certainly recommend. One issue the author highlights is how do we deal with side-effects effectively. Most software engineers are familiar with functors such as Arrays, Optionals, Results, and Promises. There exists another type designed with the intent to deal with side effects. This is the IO type.

You can find all the code for the article in the following playground file.

First let’s take a look at an example of an impure function.

Nothing in the function signature indicates that this is indeed a function which has a side-effect. But we can make the function pure by simply making a minor change to the function signature, (Int) -> () -> String?, and updating the implementation.

Had we not surrounded the the function in a closure, getFromStorage’s output would be dependant on external circumstances. The very definition of a non-pure function. With the closure in place, we will always get the same output based on our input (another function that accepts an Int and returns a String).

That solves one issue, but is still not very useful to us. This, however, is where we can make use of the IO type. Below is a simple implementation.

And our implementation of compose.

“IO differs from the previous functors in that the value is always a function. We don’t think of its value as a function, however — that is an implementation detail and we best ignore it. What is happening is exactly what we saw with the getFromStorage example: IO delays the impure action by capturing it in a function wrapper. As such, we think of IO as containing the return value of the wrapped action and not the wrapper itself.” Excerpt From: Mostly Adequate. “mostly-adequate-guide”. Apple Books.

I think this is best demonstrated with examples.

We just defined our in-memory storage and some pure functions that will be used to manipulate our stored value. Our storage can easily be changed to an external storage as long as the function has the same signature. Did someone ever say mocks are code smell? No need to define complicated interfaces. Our function signature is our interface. If you would like to know more, I would suggest the following video on functional design patterns. However, now let’s construct our IO type.

Now our IO type has the signature () -> String which means we have not done any of the transformations yet. We have taken the following:

(Int) -> (String?) -> (String?) -> (String?) -> (String?) -> String?

And compressed it into:

() -> String?

Once we call the execute method on the IO type, we compute the result by actually executing the side-effect and all the transformations finally take place. Think of it like pulling the pin from a grenade.

“Our mapped functions do not run, they get tacked on the end of a computation we’re building up, function by function, like carefully placing dominoes that we don’t dare tip over. The result is reminiscent of Gang of Four’s command pattern or a queue.” Excerpt From: Mostly Adequate. “mostly-adequate-guide”. Apple Books.

So like the guys at Point Free like to say “What is the Point”?

Before we answer the question above, lets have a look at the types we are familiar with and the purposes they serve:

Optional Type

In programming languages (more so functional programming languages) and typetheory, an option type or maybe type is a polymorphic type that represents encapsulation of an optional value; e.g., it is used as the return type of functions which may or may not return a meaningful value when they are applied. https://en.wikipedia.org/wiki/Option_type

Result Type

Provides an elegant way of handling errors, without resorting to exception handling; when a function that may fail returns a result type, the programmer is forced to consider success or failure paths, before getting access to the expected result; this eliminates the possibility of an erroneous programmer assumption. https://en.wikipedia.org/wiki/Result_type

Promise Type

In computer science, future, promise, delay, and deferred refer to constructs used for synchronizing program execution in some concurrent programming languages. They describe an object that acts as a proxy for a result that is initially unknown, usually because the computation of its value is not yet complete. https://en.wikipedia.org/wiki/Futures_and_promises

IO Type

Finally our IO type is indicative of a contained side-effect. Since we cannot safely assume a function has a side-effect by just looking at the function signature, we absolutely know by the type being returned. This helps us write more maintainable code with code that is easier to test. We can also identify and push our side-effects to the end of our architectural boundaries.

“At any rate, we can finally play with impure values without sacrificing our precious purity.” Excerpt From: Mostly Adequate. “mostly-adequate-guide”. Apple Books.

You can find the implementation of the above in the following playground file:

If you enjoyed the article tweet to me please as it encourages me to write more.

Tyrone Michael Avnit

Written by

Polyglot Programmer. Specialising in iOS development. Liverpool supporter & Comrades Marathon runner. Speaker & iOS engineer @civickey.