For the remainder of this post I’m going to assume that you roughly understand what a monad is, or can at least understand how you would use one in a codebase that has an actual
main method. If not, fear not, there are a number of wonderful resources out there to teach you this, however, unless you have that context, the rest of this post will seem useless, complicated, or both.
I could sit here and talk about the theoretical underpinnings that make Free/r monads interesting, but there are far more qualified people than I to talk about such things. And while they are certainly interesting in their own right, I want to take a step back, de-emphasize the theory, and talk about something more concrete. And while you may have never exactly encountered the scenarios I’m about to lay out, the essence of the frustration should seem eerily familiar.
Have you ever gotten the requirements of a project, coded it, delivered it to the stakeholder(s), and had them accept it without a fuss the first time around? Yeah, me neither. They always want to tweak something between that v0 you hand them before arriving at whatever becomes the stable solution for the time being
Now, of course, this is fine. We want to satisfy our customers and write software that actually does what people want it to do, but when designing this stuff, there are certain decisions you can make that make your own life difficult if you try to change it later.
In most cases, when people ask you to make something, there’s a very small set of its implementation that they care about, and that’s usually the original API that they actually specify. Technical debates about whether you should store the data in Postgres or on the Filesystem, or debates about whether caching is done in memory or in Redis, are things you get to decide. Your PM’s don’t give a shit.
So given that you’re building software for them in the first place, why would you spend any time on the implementation details before getting the high level semantics down right?
Of course, that stuff still has to get done before you can actually ship the code, but a demo is worth a thousand requirements meetings. People realistically don’t know what they want until they see it, so can we somehow show them a version of what the system will look like before we get to all the grimy engineering details of making it fault tolerant, performant, etc.?
Before we answer that, let’s take a look at another situation.
Testing is an interesting subject to talk about in Haskell because with such a sophisticated type system we often find that when our software compiles it will “just work”. Now this isn’t technically true because any monomorphic function
Foo -> Bar -> Baz can have many different implementations that satisfy that type signature, and almost certainly, at least one of them is wrong.
So while there are entire categories of tests we don’t have to write that people writing Ruby or JS have to, the number of tests we have to write is still nonzero. Now, for pure functions we have some pretty world-class tooling such as
hedgehog which I’ve been favoring more recently, but these things are primarily focused on testing data transformations.
However, sometimes we want to be able to write a test that ensures that actions produce other actions that may not have a representation in the return type of your function. After all, how would you go about testing whether a function
Foo -> IO Bar worked correctly? If it was supposed to log the value of type
Foo before grabbing the right
Bar out of the database and returning it, how do we make sure that log event happened?
It’d be nice if we could plug and play logging implementations depending on whether we were in a test environment or the real application. But to do that we need to be able to parameterize part of that function. The trouble is that we know this function needs to take a
Foo as an argument and yield a
Bar as a result. So what else is there to parameterize? Can we parameterize the monad it’s running in to be
Foo -> m Bar and then depending on the environment instantiate
m with either
IO or some test monad?
This is roughly how the strategy of mocking things works in OOP. But we can’t let them have nicer things than us. Is there a way we can accomplish all the same things?
Let’s visit one final frustration before we get to the answer.
Prove you can’t Launch Nukes™
If you spend even a little bit of time in Haskell you’ll start to lean pretty heavily on type signatures to get an idea of what a particular piece of code is doing.
Asset -> Price probably gives you the price of that asset, which is loads better than a comparable signature
String -> Double. Not only because it constrains the input and output types, but also makes a good faith effort to describe what the function is doing in a very “TL;DR” manner.
So what is the least descriptive type signature ever?
Well, given that Haskell is a general purpose programming language, and than Turing Completeness makes it such that anything that is computable should be expressible, it stands to reason that the type signature of our
main method is about the most useless type signature ever, since any program at all can satisfy it. So what is that type signature?
Any program at all can inhabit that type. This means without scrutinizing its contents we have no idea what it does. And while
() is a somewhat worthless return type since it only has one inhabitant, it’s not the scariest part of this type signature. The structurally similar
Identity () is a lot clearer about what it can, or more importantly can’t, do.
So why is
IO so scary? Because it’s more or less like giving root to someone. Once given control, it can do whatever it wants before giving control back to the caller.
Nevertheless, if we want to write useful programs we need to be able to do things that require
IO. But what we’d want to do is constrain the types of
IO it can do, and make it clear in the type signature that those are all it requires.
So we want some system of specifying which types of
IO, henceforth referred to as effects, in such a way that if we needed to add more effects to that function we could easily do it, but still be forced to say that is what we are doing.
What is Eff?
Eff is a structure with some beautiful theoretical underpinnings that allows us to deal with the above phenomena in a tractable and scalable way. It’s main value proposition is bisecting your effectful code into a “what” and a “how”, along with a method of choosing the “how” at a different call site than the what. There are numerous implementations of this idea, and the one that we’ll be referencing throughout the rest of this post is freer-simple.
Your business logic cares about the “what”, but your execution environment is what cares about the “how”.
Minimum Viable Eff effect
So what’s going on here? We have a datatype that describes some
Console effect that has two operations:
GetLine which is some effectful way of getting a
PutLine which takes a
String and does something with it and gives you back
But the magic is not in the datatype it’s in the following function definitions that are generated automatically by the Template Haskell
What is happening here is that
r is a type-level list of effects, and the
Member constraint is saying that
Console must appear in that list somewhere. Finally,
send is merely allowing us to use these effects together with each other in a “mix and match” fashion, without having to worry about the machinery that keeps all of this type-safe.
What this does is it takes the constructors for that datatype and “injects” them into the
Eff r monad that is completely polymorphic in r with a constraint that the Console effect is in there somewhere.
Keep in mind, we haven’t said shit about how this thing is supposed to get or put lines anywhere. We’ve just said, “hey, we want to do get and put to the console, and we’ll worry about how to do it some other time”. So let’s consider the following program
Neat. This program, from a structural standpoint looks like how we would code a bot that repeatedly asks for your name and then greets you. We aren’t bogged down with the details about how to get that string or send out the greeting. The code only specifies the high level design of the program. The skeptical reader might say, well we can do that without all this Eff machinery by just pulling out
getLine :: IO String and
putLine :: String -> IO () to their own function. And not only that, but
base already does this for us. So what have we really accomplished?
The answer is that not only have we packed that logic elsewhere, but we haven’t even committed to a particular implementation yet! There are no typed holes, no
undefineds and we still can have a program that typechecks without having committed to these details.
That said, this program is still incomplete and won’t yet run, precisely because we haven’t actually told it how to handle these gets and puts.
So what are we going to do in the regular program case? The aforementioned functions in base will do just fine I think:
This is all great, but
greetBot isn’t actually a program of type
Console a. Instead, it is one that that has the type
Eff r a where the only requirement on
r is that it is a list that contains
Console in it somewhere. The minimum concretion of
greetBot could have type
Eff '[Console] (), but the point here is that it is not limited to that, and can be combined at will with other effects, that, in conjunction, build up a much larger list.
Once this list is built, though, we need a way to independently interpret these effects. We also need to do this in such a way that we can define the handler with no knowledge of anything besides the source and target effects. We want this so that our effects can remain isolated from one another but can be composed together to interpret more complicated programs.
This is where the value of effect libraries such as
freer-simple start to shine.
freer-simple gives us some functions to be able to take the above action mapping and use it in the context of the
translate :: (forall a. f a -> g a) -> Eff (f ': r) b -> Eff (g ': r) b translate = _ runM :: Eff '[m] a -> m a runM = _ -- to close the gap interpretConsoleInIO :: Eff '[Console] a -> IO a interpretConsoleInIO = runM . translate consoleToIO main :: IO () -- this translation to IO happens at the edge of our program main = interpretConsoleInIO greetBot
Great! But how do we test it? I promised testing capabilities, I should deliver on it. To really do that we need to tweak the original program just a bit so we can just test a single iteration of it.
We have to do this because if we try to test a program that loops forever the test will never terminate itself. So we’ll actually be testing greetBot’ here.
What is a natural way we might want to test this? Well, the main invariant here is that the thing emitted over the put should at least contain the name obtained via the get. Let’s write out a property test for this.
So we’ve run into our first issue, we want to be able to supply a name that was given to us from the test environment to our program directly. So we want
greetBot' to read for its
getLine call and write for its
putLine call. Can we interpret our Console action into more than one effect? Turns out yes.
-- freer simple provides some out of the box reader and writer effects that behave the same way -- that their identically named monads in `base` behave consoleToReaderAndWriter :: ( Member (Reader String) r , Member (Writer [String]) r ) => Console a -> Eff r a consoleToReaderAndWriter action = case action of GetLine -> ask PutLine s -> tell s
And with the appropriate functions from
freer-simple we can interpet this down to a pure value!
-- used to get from Console to Reader AND Writer reinterpret2 :: (forall a. f a -> Eff (g ': h ': r) a) -> Eff (f ': r) b -> Eff (g ': h ': r) b reinterpret2 = _ -- used to discharge reader runReader :: env -> Eff (Reader env ': r) a -> Eff r a runReader = _ -- used to discharge writer runWriter :: Monoid w => Eff (Writer w ': r) a -> Eff r (a, w) runWriter = _ -- used to discharge Eff machinery around a pure value run :: Eff ' a -> a run = _ -- to close the gap between our mapping and the function we want interpretConsoleInReaderWriter :: String -> Eff '[Console] a -> (a, [String]) interpretConsoleInReaderWriter env = run . runReader env . runWriter . reinterpret2 consoleToReaderWriter
OK. So now that we’ve defined our testing interpreter we’re ready to complete that property test.
-- hedgehog property test prop_nameMatchesGreeting :: Property prop_nameMatchesGreeting = property $ do name <- forAll nameGenerator let consoleOutput = snd $ interpretConsoleInReaderWriter name (greetBot' $ pure ()) case consoleOutput of  -> failure -- nothing was emitted (line:_) -> assert $ name `isInfixOf` line
Boom! We just wrote a test that tests effects working properly within the context of our business logic.
Let’s recap what just happened. With quite minimal overhead we defined a new capability
Console to be used throughout our application. We defined the interpreter we want it to use in the production environment, as well as an interpreter that allows us to control inputs and measure outputs in our test environment. Additionally, we gained the ability to write business logic without committing to a
Console implementation. And finally, our business logic more explicitly states the capabilities it needs.
Can we do this to everything?
The short answer here is yes. You absolutely can go ham on making effect algebras for everything in your entire codebase, but every effect you introduce gives you some extra overhead. So my rule of thumb is this: If you have some well defined semantics for your API, or you need to be able to mock it out for testing, it’s a pretty good candidate for an Eff effect. Otherwise, you probably lose more than you gain from this.
All that said, some people have taken this much further and have some really interesting results.
Time for the Majors
OK. So the example above is pretty compelling (at least to me), but when was the last time you actually wrote a program that only did reads and writes to the console. It was probably the first thing you learned to do when you learned to code so it doesn’t really accurately reflect the problems you deal with in industrial software, right?
Wrong. There are some reasons that you may not want to use this technique in production and I’ll get to those at the end, but inability to express all of the things that you need is not on that list.
So what we want to do is create a server that continuously fetches prices from third parties, aggregates them some way, saves them, and then serves up the result on request.
It might be tempting to say that a web service that does this seems too simple to be useful, however, if any of my colleagues were reading this, they’d tell you it looks awfully similar to a service we have currently running in production.
Let’s write some new effects!
Ok. So immediately what jumps out at me is that since the problem statement was intentionally vague about the third parties in question, and the method of saving, those are the candidates for…wait for it…free-monadification.
data AssetPairing = _ data Price = _ data Exchange = _ data PriceFeed a where FetchPrice :: Exchange -> AssetPairing -> PriceFeed Price makeEffect ''PriceFeed data PriceStore a where SavePrice :: AssetPairing -> Price -> PriceStore () GetMostRecentPrice :: AssetPairing -> PriceStore Price makeEffect ''PriceStore
Time to make the PM’s happy
With just the code above we’re actually ready to write our business logic.
For the daemons continuously fetching and saving we have this:
allExchanges :: [Exchange] allExchanges = _ getPricesFromAllSources :: Member PriceFeed effs => AssetPairing -> Eff effs [Price] getPricesFromAllSources assetPairing = for allExchanges $ \exchange -> fetchPrice exchange assetPairing aggregatePrices :: [Price] -> Price aggregatePrices = _ -- some fold fetchAndSave :: (Member PriceFeed effs, Member PriceStore effs) => AssetPairing -> Eff effs () fetchAndSave assetPairing = do prices <- getPricesFromAllSources assetPairing let agg = aggregatePrices prices savePrice assetPairing agg
And for our request handler we have this embarrassingly small piece of code here. And since we actually want to wire this up to a real Yesod handler, let’s go ahead and do just that.
The astute reader might notice that we’re in the wrong monad here. We need to go from our
Eff defined logic to the actual handler here.
The above code definitely cheats. Freer monads don’t save us from having to write all the grimy engineering details, but it does save us from having to interleave those details, or even commit to them. But when we actually wire into the web application, it’s time to make a commitment. After all we can’t avoid specifying how these prices will get fetched and saved in a real production environment.
Make it work
So what will our interpreters look like?
Well, since we’re fetching these prices from external parties, theres pretty much no avoiding going straight to IO, possibly with some sort of configuration for an api key.
type (~>) f g = forall a. f a -> g a -- from freer-simple data ExchangeConf = _ gdaxApiKey :: ExchangeConf -> String data GDAXResponse = _ gdaxRespToPrice :: GDAXResponse -> Price asks :: Member (Reader) r effs => (r -> a) -> Eff effs a asks = _ -- from freer-simple priceFeedToRIO :: (Member (Reader ExchangeConf) effs, LastMember IO effs) => PriceFeed ~> Eff effs priceFeedToRIO action = case action of FetchPrice exchange pairing -> case exchange of GDAX -> do -- GDAX actually doesn't require an api key for their price api, but I'm -- making this up because enough -- third party services require some sort of auth that this felt like it'd be more helpful key <- asks gdaxApiKey initReq <- sendM . parseRequest $ "GET http://api.pro.coinbase.com/products/" <> show pairing <> "/ticker?apiKey=" <> key gdaxRespToPrice <$> sendM (httpJson initReq)
Great. We now have a way to legitimately fetch prices from a real place. But do we want to hit GDAX from our CI pipeline?
type ExchangeTestbed = HashMap (Exchange, AssetPairing) Price priceFeedToReader :: (Member (Reader ExchangeTestbed) effs) => PriceFeed ~> Eff effs priceFeedToReader action = case action of FetchPrice exchange pairing -> do hm <- ask -- It's a test interpreter for a conference talk, I'm cheating totality here let price = fromJust $ lookup (exchange, pairing) hm pure price
So now we can test that our business logic saves the right data because we can control what data it gets to begin with.
Interpreters are reusable
What does the PriceStore interpreter look like? Well it depends on how we want to store the data. Here we have some choices: an sql database (postgres), redis, live memory, or some combination of those.
priceStoreToPostgres :: ( Member (Reader ConnectionPool) effs , LastMember IO effs ) => PriceStore ~> Eff effs priceStoreToPostgres action = do pool <- ask Persistent.runSqlPool $ case action of SavePrice pairing price -> insert _ -- left as exercise GetMostRecentPrice pairing -> selectFirst _ -- left as exercise type PriceCache = TVar (HashMap AssetPairing Price) priceStoreToPriceCache :: ( Member (Reader PriceCache) effs , LastMember IO effs ) => PriceStore ~> Eff effs priceStoreToPriceCache action = do cache <- ask case action of SavePrice pairing price -> sendM $ atomically $ do cacheState <- readTVar cache let newCacheState = insert pairing price cacheState writeTVar cache newCacheState GetMostRecentPrice pairing -> sendM $ readTVarIO cache priceStoreToPGandCache :: ( Member (Reader ConnectionPool) effs , Member (Reader PriceCache) effs , LastMember IO effs ) => PriceStore ~> Eff effs priceStoreToPGandCache action = case action of SavePrice _ _ -> do priceStoreToPostgres action priceStoreToPriceCache action GetMostRecentPrice _ -> -- no pg here because we're just reading, gotta go fast priceStoreToPriceCache action
Wow. So we just wrote two separate effects handlers and wrote a third one in terms of the other two. Hopefully this conveys that something you might encounter in a real world codebase can be turned into this style. This is still perhaps a simpler problem than the typical industry grade version, but it’s still more than a toy and should demonstrate the type of value you would get from doing something like this.
Why shouldn’t I use this
Alright alright, is it too good to be true? Just barely. The reasons why you may choose not to use this style in a production Haskell codebase are as follows:
- Monadic sections of your code can be slower
- Resource bracketing can’t be expressed this way
But hope is not lost, there is an alternative library that Sandy Maguire just published called polysemy that pretty much fixes both of these problems. The only reason I didn’t write this post with that as the library being studied is because I haven’t had a chance to play with it in a production codebase yet.
Freer monads have made my code way more testable, better documented, and much better decomposed than it used to be without. I am by no means saying this is the only way for you to accomplish these things but it has certainly improved my code quality by quite a margin, and yet it remains practical enough for us to deploy real-world services that use this technique to production. If you have had a tough time testing IO code or find that you get this sense of fear when you see a type signature of
a -> IO b, maybe give this a shot and see if it solves your problems.
It is also worth noting that this technique can be introduced at the edges of your existing services without it infecting everything else, however the ergonomics of it skyrocket when you refactor your whole codebase to use this technique. Happy coding.
Until next time.