Free Monad - Interpreter pattern in F#
An analysis of the Free Monad - Interpreter pattern in F# from a definition created by erdeszt and based on: http://programmers.stackexchange.com/a/242803/145941
The DSL
First we define a DSL for our actions. Each action points to the next action using the 'next
generic type,
every action has a 'next
that in turn could be a DSL, thus chaining them.
1: 2: 3: |
|
Get
returns a string which is passed to a function.id
function can be used to finish the chain.Set
doesn't return anything, so the'next
portion is the next DSL element in the chain, or a constant like()
to finish.
Note that used this way 'next
can be anything. It does not have to be a DSL value, so there is no real implication of a chain of DSLs.
This is what 3 actions in the DSL may look like.
1: 2: 3: 4: 5: |
|
val ex1 : DSL<DSL<DSL<unit>>> =
Set ("name","John",Get ("name",<fun:ex1@23-4>))
Notice how the resulting type DSL<DSL<DSL<unit>>>
is nested and not generic.
This means a strongly type function cannot process all posible values.
The Free Monad
Here comes the Free Monad ChainDSL
to the rescue.
1: 2: 3: |
|
The Do
option creates a chain of ChainDSL
s that ends with the Return
option.
This chain ends up having a type equal to the last DSL
in the chain.
This is almost like creating a List
of DSL
s (List<DSL<'a>> or DSL<'a> list
),
except that each
except that each element of the DSL chains to the next and some elements are chained using functions.DSL
in the chain can be of a different type
Lets look at the same value above with ChainDSL
:
1: 2: 3: 4: 5: 6: 7: |
|
val exF1 : ChainDSL<unit> =
Do (Set ("name","John",Do (Get ("name",<fun:exF1@49-3>))))
Compare the resulting type with the prior case: ChainDSL<unit>
vs DSL<DSL<DSL<unit>>>
.
No matter how deep the chain is, the value will always be of type ChainDSL<unit>
or ChainDSL<string>
.
But creating the chain is much more complex than before. To solve that, lets create two helper functions: get and set.
1: 2: |
|
val get : key:string -> ChainDSL<string>
val set : key:string -> value:string -> ChainDSL<unit>
Notice get
returns a ChainDSL<string>
and set
returns a ChainDSL<unit>
.
They both return a ChainDSL
chain with a single DSL
action.
With these functions we can create Get & Set operations like this:
1: 2: 3: |
|
but they are not chained together like before.
Binding it together
To chain them we will need to define a bind function for the ChainDSL. We start with a map function for DSL, thus making DSL a functor:
1: 2: 3: 4: 5: |
|
All mapDSL
does is apply the function f
to the 'next
part of the DSL
.
In other words go to the next node in the chain.
Next we define the bind function for ChainDSL, finally making it a monad:
1: 2: 3: 4: 5: 6: 7: |
|
bindChain
is similar and acts like the List.append function, it concatenates two chains of ChainDSL
s.
The difference is that the chain to be appended fChain
is passed within a function.
bindChain
navigates recursively down chainTo
and replaces the last element with the result of fChain
:
- On the
Do
sidebindChain
callsmapDSL
to apply the function to the nextChainDSL
node. - On the
Return
side it replaces theReturn a
for a call to the chain to be appendedfChain
.
In a sense ChainDSL
is actually the opposite of a List<DSL<'a>>
. In a List new elements are
inserted at the head, here they are attached at the tail end.
Now we can bind setName, getName & setResult from above like this:
1: 2: 3: |
|
val exF2 : ChainDSL<unit> =
Do (Set ("name","John",Do (Get ("name",<fun:mapDSL@87-2>))))
which is the same as this:
1: 2: 3: |
|
val exF3 : ChainDSL<unit> =
Do (Set ("name","John",Do (Get ("name",<fun:mapDSL@87-2>))))
and this:
1: 2: 3: 4: 5: |
|
val exF4 : ChainDSL<unit> =
Do (Set ("name","John",Do (Get ("name",<fun:mapDSL@87-2>))))
Using Computational Expressions
Now lets try it with Computational Expressions. First we define a builder class.
1: 2: 3: 4: 5: 6: 7: |
|
And now we use the computational expression like this.
1: 2: 3: 4: 5: |
|
val exF5 : ChainDSL<unit> =
Do (Set ("name","John",Do (Get ("name",<fun:mapDSL@87-2>))))
The Interpreters
Now we are going to create an interpreter to execute the AST created.
This first version is very simple it does not store or retrieve any values, just prints out the commands.
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: |
|
|
This next version actually stores and retrieves the values in a Map
object, and when finished prints its content.
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: |
|
|
A slightly longer example:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: |
|
Output:
|
Return value:
|
Trying to replicate this last example without the computational expression requires explicitly nesting some of the calls.
It would look like this:
1: 2: 3: 4: 5: 6: 7: 8: |
|
Output:
|
Return value:
|
Two in one
So, do we really need two types, the Free Monad and the DSL?
I do not think it is necessary, the free monad helps in binding the elements of the DSL.
The same can be achieved just by adding the Return
option to the DSL.
Here is the same implementation with just the DSL type:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: |
|
There you have it the DSL definition, helper functions, the bind function and the interpreter. Here is the last example again.
1: 2: 3: 4: 5: 6: 7: 8: |
|
Output:
|
Return value:
|