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 while still writing code with pure functions? Most 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.
The code for below can be found in the following playground page.
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.
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. Anyway that is enough for another article, but 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.
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 the purposes they serve:
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
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
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
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.
You can find the implementation of the above in the following playground file