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 ...
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.
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."