Haskell - Extracting IO

Posted on July 30, 2020

There are more articles out there discussing this topic but this is my take on it. This topic can be long and complicated but I’ll try and extract a section of it that we can focus on.

Let’s do a contrived example. Let’s say we have a function that reads a file, processes it by capitalizing all the characters, then writes the results to another file. This is the first iteration of this function.

This adequately performs the task we want. The problem comes when we want to test the text processing without interacting with the file system. Sure, we can unit test the text processing it self but what if we want to see how the text processing behaves in the processFile function?

I learned mtl, tagless final encoding, and started reading about free monads, fused-effects, etc to be able to extract IO out; among other things. Then, I totally ignored an easier technique. Which is to make the “side-effecty” functions into a parameter, and give it a basic type constraint of Monad instead of IO, like this

We can use some type alias to make it less confusing.

We’ve extracted out the IO. Yaaay! This function is now more flexible. We can pass in functions as long as they have a Monad instance. If we go back to our IO implementation we can provide it with functions from relude.

Another advantage of this technique is we can use another library and don’t have to restructure our program. Let’s say we ended up dropping relude and using prelude instead. We can massage the writeFile and readFile functions from prelude so it can fit our program. Like so

In testing, we can provide it different functions.

Here we’re using IORef but you can use Map, List, StateT, WriterT. It’s totally up to you. Whatever suits your use-case.

This technique can take you a long way! It is also a good complement to techniques like mtl and tagless final encoding

References

Invert Your Mocks! - Matt Parsons