I am reading this excellent article by Mark Seemann.
In it, he gives a simple demonstration of using the free monad to model interactions using pure functions.
I understand it enough to be able to write such a program, and I can appreciate the virtues of such an approach. There is one bit of code though, that has me wondering about the implications.
let rec bind f = function
| Free instruction -> instruction |> mapI (bind f) |> Free
| Pure x -> f x
The function is recursive.
I may (probably) have missed something fundamental here that means that the recursion never has to go very far, please let me know.
I think the answer to the first two questions is "it depends" - there is some overhead associated with free monads and you can certainly run into stack overflows, but you can always use an implementation that avoids this and if you're doing I/O, then the overhead probably is not too bad.
I think the much bigger issue with free monads is that they are just too complicated abstraction. They let you solve one problem (abstracting over how you run code with lots of iteractions), but at a cost that you are making code very complicated. Most of the time, I think you can just keep a "functional core" as a nice testable pure part and "imperative wrapper" around it that is simple enough that you do not need to test it and abstract over it.
Free monads are very universal way of modeling this and you could use a more concrete representation. For reading and writing, you could do:
type Instruction =
| WriteLine of string
| ReadLine of (string -> Instruction list)
As you can see, this is not just a simple list - ReadLine
is tricky, because it takes a function that, when called with the string from the console, produces more instructions (that may, or may not, depend on this string):
let program =
[ yield WriteLine "Enter your name:"
yield ReadLine (fun name ->
[ if name <> "" then
yield WriteLine ("Hello " + name) ] ) ]
This says "Hello " but only when you enter non-empty name, so it shows how the instructions depend on the name you read. The code to run a program then looks like this:
let rec runProgram program =
match program with
| [] -> ()
| WriteLine s :: rest ->
Console.WriteLine s
runProgram rest
| ReadLine f :: rest ->
let input = Console.ReadLine()
runProgram (f input @ rest)
The first two cases are nice and fully tail-recursive. The last case is tail-recursive in runProgram
, but it is not tail recursive when calling f
. This should be fine, because f
only generates instructions and does not call runProgram
. It also uses slow list append @
, but you could fix that by using a better data structure (if it actually turned out to be a problem).
Free monads are basically an abstraction over this - but I personally think that they are going one step too far and if I really needed this, then something like the above - with concrete instructions - is better solution because it's simpler.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With