Free and Freer Monads: Putting Monads Back into Closet
okmij.org
okmij.org
Freer Monads, More Extensible Effects
"The ambition is for Eff [the extensible freer monad, the monad of extensible effects] to be the only monad in Haskell. Rather than defining new monads programmers will be defining new effects, that is, effect interpreters."
It is only composable for commutative effects. Once you introduce non-commutative effects like throw/catch or nondeterminism, the semantics of a block of code using more than one effect depends on the order in which interpreters are run.
Monad transformers escape this issue by allowing code to specify the effect nesting order in the type. They're no panacea either, of course. But they currently allow somewhat better code reuse and abstraction properties than the Eff approach does.
If Eff ever gets the ability to control the semantics of non-commutative effects, it will have a much more compelling story.
I failed to mention though that Eff would also benefit from some boost to type inference for open sums. Currently it's throwing up hands for component effects with polymorphic inner type parameters, like `State s` or `Reader r`, so we can't do things like `put 0`, but only `put (0 :: Int)` (in the absence of other type annotations in outer scopes). It's fortunately not an impossible problem; we just need to allow for unique projections from sums in our type-level machinery (this requires some annoying boilerplate currently, but maybe GHC 8.0 will make it easier with the new kind system).
I like to think of free monads and related constructions as dependency injection "turned inside out". With dependency injection, you configure a foozle in various ways, and then the foozle controls how and when to use the injected components. The foozle is on the outside; the dependencies, once injected, are on the inside.
With a free monad, the foozle (the "logic" of the application) is on the inside, and an outside interpreter is the one that services all the "effect requests". The foozle is "run" by the interpreter.
That's the basic idea. Then there are practical concerns of how to make it fast enough, of how to be able to handle different combinations of effects (environment reads, state, io requests...) without requiring boilerplate, and so on.
Say you're automating a bio lab. You're writing software to control evacuations. The lab machinery can execute two actions (1) Alarm (sets off an evacuation alarm in an area) (2) Sterilize (burns an area with fire). Obviously we would like to call (1) for an area well before we call (2).
To these two actions we can add one of our own: (3) Wait (for some number of seconds).
Now we write a function (using Haskell types, but they're easy to read):
evacuate :: ContaminationInfo -> IO ()
This function takes in the state of a lab (what areas have been contaminated, what areas are at risk, etc.) makes an evacuation plan, and executes it.Now, the first things you notice is that this is a horrible function. How could we test it? We would need an actual lab (or mocks, but that's no fun).
Let's try again.
makePlan :: ContaminationInfo -> [Action]
(Where action is either `Alarm Area`, `Sterilize Area`, or `Wait Seconds`)This is way better! Now we can write interpreters for `[Action]`.
testContaminationPlan :: [Action] -> TestResults
prettyPrintContaminationPlan :: [Action] -> String
graphContaminationPlan :: [Action] -> SVG
runContaminationPlan :: [Action] -> IO ()
Note that all of this can be done in Python, which is pretty cool! Sadly we can't write `makePlan :: ContaminationPlan -> [Action]` in Python though. It would have to be `makePlan :: ContaminationPlan -> IO [Action]`, which means that there could be side effecting functions that slip by the programmer and aren't reflected in the return type. If one of these is Sterilize someone could have a bad day:(Now lets say our wonderful engineers build us an excellent new action: (4) Scan (check an area for risk of contamination).
Now our [Action] plan starts to break down. Before an [Action] was very simple. An example might have been [Alarm Area1, Alarm Area2, Wait 200 Seconds, Sterilize Area1, Sterilize Area2].
But our scanner allows later actions to depend on the result of previous actions! [Action] isn't sufficient to model that -- how would our interpreters know how to thread things together (our goal is to be able to do things like start scanning an at-risk room like crazy, but not actually evacuate it unless contamination appears).
If only we had some way for our data type to reflect that some actions might need to be fed into others? Hmm, I wonder what we could use for that ...