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: |
|
Getreturns a string which is passed to a function.idfunction can be used to finish the chain.Setdoesn't return anything, so the'nextportion 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 ChainDSLs 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 DSLs (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 ChainDSLs.
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
DosidebindChaincallsmapDSLto apply the function to the nextChainDSLnode. - On the
Returnside it replaces theReturn afor 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:
|