Freer doesn’t come for free

The question on how to best structure functional programming applications in either Haskell or Scala is still out with various ways to achieve modularity, encapsulation and testability. In the past I have been an advocate of the `Eff` monad, based on Oleg Kiselyov’s work on “Freer monads, more extensible effects”.

However, after being involved with 2 large applications (both in Scala and Haskell) using a “Freer monad” I would not recommend this anymore.
Why is that?

The benefits of the Eff monad

Sandy Maguire has recently wrote a blog post explaining why he liked freer monads so much, but also trying to dispel some criticisms. He writes:

In short, freer monads let you separate the high-level “what am I trying to do” from the low-level “how to actually do it.”

This is true, but this is a valid observation for any system where you use a “interface/implementation” dichotomy. You get it with regular traits/case classes in Scala or MTL typeclasses / monad transformer instances in Haskell. The details vary mostly in usability.

Another advantage is the ability to “interpose” an effect, meaning that we can modify a program before even interpreting it. For example you append the string ”[THIS MY LOG]” to any message printed out by a program using a Logging effect. For Scala and case classes this would mean that you would create a wrapper/decorator around the methods of your interface:

case class MyLogger(delegate: Logger) extends Logger {
def info(s: String): IO[Unit] =
delegate.info(“[THIS IS MY LOG]”+s)
}

The major difference is also in terms of usability. With the Eff monad you can just redefine one “method” and leave the other unchanged while with a wrapper you need to redefine *all* the methods. But this is not really a blocker.

One other thing look quite appealing with the Eff monad, compared to MTL/monad transformers. The interpreters are functions which can be composed to create larger interpreters and you don’t need to rely on the typeclass resolution algorithm to “wire” an interpreter for you. This gives a bit more flexibility when it comes to testing because you can more easily replace a “production” interpreter by a “mock” one. That’s the theory at least, more on that below.

Finally I think that the main benefit for the Eff monad is why I started looking at it in the first place. This is another take on monad transformers, giving you effects which can be interpreted in different orders (not necessarily in the order defined in the “stack of effects”) and just one data type to support many effects instead of several distinct ones. But this is not what we should use to structure our programs.

Why? First of all because we get the benefits in terms of structuring programs just by using a regular “interface/implementation” separation with traits and case classes in Scala, “records of functions” and (instantiation) functions in Haskell. Are the 2 approaches completely equivalent then? No because effect libraries come with a fair amount of downsides.

The downsides of the effect libraries

The downsides I am going to mention now might just be temporary, there is still exciting research going on and new projects coming up to improve the situation!

Boilerplate

Creating effects require the creation of a dedicated data structure + smart constructors + an interpreter for each case of the datatype which is why some libraries give you code generation, for example the freestyle library in Scala.

Performance

Freer monads reify a program into a datastructure which is reduced later on. This building / destructing is costly but it generally doesn’t matter. Besides this can be avoided by using libraries like fused-effects in Haskell or effekt in Scala. Besides, as Sandy points out, if we use effects to structure applications they are likely to represent components having some kind of `IO` action which will be a lot slower than interpreting a data structure.

Bracketing

This is a more serious problem for production applications. We had a memory leak for example where, in case of an exception, some data was accumulating in memory but never cleaned-up:

start “action” — keep track of the ongoing action in memory
doAction
end “action” — remove the label

If doAction throws an exception, the label “action” stays forever. Unfortunately it is not possible to implement a proper bracketing action with “classic” versions of the Eff monad (try it!):

bracket ::
Eff r a -- acquire
-> (a -> Eff r ()) -- finalize
-> (a -> Eff r b) -- use
-> Eff c -- result

This can be solved. For example the fused-effect and effekt libraries rely on a different encoding (akin to delimited continuations) permitting this kind of pattern. This doesn’t for free (haha) though! The types involved in fused-effect are more complex and effekt is a lot more elegant with upcoming features (implicit functions) from Scala 3.

There is a limited work-around for freer if “your allocation and cleanup code only requires IO” as mentioned in Sandy’s article but this wouldn’t work in the example we had at work.

Note: Jonathan Brachthaüser (the author of the effekt library) has been working with Daan Leijen (creator of the koka programming language supporting effects) to use a linear type system in order to control resources usage.

Concurrency

Kind of a big issue here. When we run programs talking to external services it is very important to be able to run them asynchronously. But all the concurrency primitives, concurrently/parTraverse/race/… are “low-level” in Haskell and Scala. They know what to do for IO a actions not for general Eff r a actions. Sandy provides a trick to solve this issue withfreer but this still means that you need to re-implement some of the concurrency primitives. I think that a better answer to this question comes with the extension of the Eff monad to an `Applicative`. I tried to do something like this
 in the Scala Eff library but Jonathan is working on a much better version for effekt.

The types

Extensible effects in both Scala and Haskell make a really nice use of the type system, using a list of types to track the effects to handle. They also use “rank-2” types for interpreters because interpreters are functions which should be able to interpret Eff r a *forall a*. Unfortunately this comes sometimes at a high price in terms of developer productivity. Depending on the library or the use case you might be left with huuuuuge errors messages and spend lots of time and energy trying to figure out what needs to be fixed. This really gets in the way of refactoring large programs.

Not only that but it will take you a good amount of time to explain how things work to beginners. In principle am totally fine with that but only if the cost comes with a massive benefit.

The wiring

This one is dear to my heart. I think that carefully crafted components, hiding implementation details, are essential to the maintenance of large applications. Those components have dependencies making your whole application a (hopefully acyclic) directed graph. When testing for correctness or performance, diagnosing issues, trying out new ideas on the command line it is very useful to be able to tactically replace some components by others in that graph. And while I think that most of the recommended approaches: effects, MTL, normal classes, just functions give us “composability” they don’t necessarily give us “re-composability”, meaning the ability to easily “recompose” a given set of components/interpreters/transformers stack to get a different meaning. Look at the gymnastics that people tried to do with monad transformers for example.

“freer” monads are not exempt from this. Sandy’s example of a top-level interpreter is

main = runM
. runRedis
. runFTP
. runHTTP
. runEncryption
. redisOuput @Stat mkRedisKey
. postOutput @Record mkApiCall
. batch @Record 500
. ftpFileProvider
. decryptFileProvider
. csvInput “file.csv”
$ ingest

This is really neat but what if you want to batch things differently with a different way to post your outputs? You will have to rewrite the whole function. Or maybe you start passing some of those interpreters as parameters to the `main` function? Then the types can become annoying in real-life because those interpreter functions are slightly more complicated than regular functions. This eventually leads to duplicated code that’s harder to maintain. It can also get more complicated if you want to use 2 different
Redis databases for the same effect.

This is why I proposed a different approach with registry, basically encoding in a datastructure + an algorithm how different functions are being composed so it is easy to say “just change *this part* of the composition”.

Mixing low-level and high-level

Another drawback of effects for structuring applications is that everything is an effect. From RedisEffect to State Blacklisted, IO orException HttpError. This makes programs written in that style harder to understand in my opinion. The separation of components as case classes or “records of functions” potentially parameterised by an appropriate monad F[_] for the return type of the functions is a lot better IMO. Then you can specify when instantiating your component that F needs aMonadIO, or MonadError HttpError instance if you want.

This is actually one place where the Eff monad could be re-introduced if it supports bracketing and concurrency although I find IO quite compelling in that it hides totally the implementation of the component declaring “it has some kind effect, that’s all you need to know” (others might strongly disagree with that view :-)).

Conclusion

I don’t regret the detour I took to understand and use the Eff monad in anger to structure applications but I really don’t think that the benefits outweigh the costs. I also think that MTL/transformers are not the way to structure applications because of their inherent rigidity.

But hey, this is actually good news! Because I think that both in Haskell and Scala you can build modular, extensible, understandable, easy-to-refactor applications using simple constructs, the resurgence of the so called “final tagless style” is not really much more than that: interfaces and implementations, with the twist that they are parameterized by a type parameter F[_] for added abstraction. In some cases you might reach for a library like registry to help with wiring but I get a few field reports telling me that this is not even necessary in many apps/services.