Learning FP using Language-Ext / A usecase for Free Monad
See original GitHub issueI’ve just started on track to be reformed programmer (Louthism!) and eager to handle this usecase functionally in general and using Language-ext in particular.
Suppose we have following source data in json format:
[
{"Type":"Asset","Id":"Account Recievable","Amount":50000},
{"Type":"Asset","Id":"Cash In Hand","Amount":10000},
{"Type":"Asset","Id":"Bank ABC","Amount":100000},
{"Type":"Expense","Id":"Salary","Amount":30000},
{"Type":"Expense","Id":"Office Rent","Amount":6000},
{"Type":"Expense","Id":"Utilities","Amount":4000},
]
And want output data computed based on source data:
[
{"Type":"Computed","Id":"Total Assets","Amount":160000},
{"Type":"Computed","Id":"Total Expenses","Amount":40000},
{"Type":"Computed","Id":"Balance","Amount":120000},
]
We want to provide with ability to declare what other basic values on one wants to compute.
To compute desired output from source data, following declaration/specfication in json format might be a reasonable start:
[
{
"Type":"Compute",
"Id":"Total Assets",
"Operator":"Sum",
"SourceType":"Asset",
"Remarks":"Sum all with type Assets"
},
{
"Type":"Compute",
"Id":"Total Expenses",
"Operator":"Sum",
"SourceType":"Expense",
"Remarks":"Sum all with type Expense"
},
{
"Type":"Compute",
"Id":"Balance",
"Operator":"Expression",
"SourceType":"{Total Assets} - {Total Expression}",
"Remarks":"Compute Balance"
},
{
"Type":"Print",
"Remarks":"Print messages after substituting computed Ids in each message format string",
"Messages":[
"Total Assests : {Total Assets}",
"Total Assests : {Total Expenses}",
"Balance (Total Assets - Total Expenses): {Balance}"
]
}
]
After digging through Langugage-ext related resources (wiki, forums, samples, tests, etc.), it seems this problem can be modeled similar to BankingAppSample with BankFree free Monad.
Essentially we need to write “Interpreter” for the Json Specfication/Declaration to transform input data source to desired output.
Need some help on whether am thinking in right direction and any resource that can help addressing this usecase or to the extent where someone willing to give it a shot.
(Note: On advice of @louthy, this post is copied from gitter post on 6th Apr 2018.)
Issue Analytics
- State:
- Created 5 years ago
- Comments:8 (4 by maintainers)
Top GitHub Comments
@imranypatel So, if you want to do this, as well as it can be done - and learn the functional thing on the way. Then (based on what you’re asking for) I would do the following (btw, I have implemented this exact setup twice in the past few weeks, so I know it will work, but it’s going to be a steep learning curve).
Here’s a working gist of what I’m going to talk you through:
https://gist.github.com/louthy/b3a40c7f5f1bc1a8431c770230800ecb
Firstly the idea of building a Free monad is exactly how this will work well. Note however that lang-ext doesn’t have generalised free-monad support (one day I’ll work out how to generalise it, but it isn’t there now). So, each time you want to use a technique like this you have to do the plumbing yourself.
Next, to do the expression stuff, then you should use LanguageExt.Parsec - this is what it’s built to do. It can build a mini-DSL language that you can use to make either a simple expression system right up to a fully fledged language.
The parser you build with LanguageExt.Parsec should ‘compile’ to the same Free monad. The Free monad type will need to have cases to handle expressions. This will allow your entire process to be one computation that both runs the operations but also runs the expressions.
Once you have the Free monad definition you need to define the Interpreter to run it.
So, first I’ll start off with the Free monad definition. The first step is to create ‘an algebra’ for all the operations you want to perform. What this means is a ‘discriminated union’ the describes every operation. C# doesn’t support discriminated unions, so we use inheritance.
Below is the core type
Transform<A>
and its ‘cases’:Note how most of the classes have a
Next
field which is aFunc
. That is how the operations are chained together. A bit like continuations. OnlyReturn
andFail
don’t haveNext
, and that’s because they terminate the computation.For the type to become a monad you need to define the standard monad operations:
NOTE: Every time you add a new case to the type you need to add it to the
switch
in theBind
function. The important thing here is that each one is basically the same, it’s recreating the same case type, but with a newNext
function, and each time you do it it’s the same. So it’s quite easy to do.What this does is allow non-
Transform<A>
behaviours to be injected without defining new cases. This enables LINQ operations, etc.Now, for convenience we create a static type called
Transform
which is a factory type for creating newTransform<A>
types. The important thing with these factory functions is we don’t need to supplyNext
, it usesReturn
as the defaultNext
function.Now, some of those functions are more complex than just creating new
Transform<A>
types, and that’s because they support scripting (SourceType
inCompute
andMessages
inPrint
). What they do is the parse the text first into aScripting.Expr
, then they compile the expression into aTransform<A>
. That means the scripts are running in the same context as the bulk operations.To achieve this I build a language parser using
LanguageExt.Parsec
. It does a lot for free, you’ve almost got an entire language here. I won’t walk you through all of that, but essentially theScripting
type builds the core parsers, an operator precedence and associativity table, and then callsbuildExpressionParser
which does the hard work of making a robust expression parser. So, callingScripting.Parse(text)
will give you aScripting.Expr
which is an abstract syntax tree.You then pass the expression to the
Compiler.Compile
function which walks the syntax tree building a singleTransform<A>
.What’s nice about this approach is you will get proper contextual errors out of the thing if there are syntax errors, or typos, etc. And it can work in the same context as the bulk computation. That means it can access the rows, or whatever.
So, now we have a way of manually creating a
Transform<A>
, we need a way of creating it from the ‘schema’ JSON. Now, I’m not going to do the work of loading JSON for you, so take a look at theData
type:It has your original test data in it - but also a simple
Load
function that takes a sequence ofIOperation
values and then compiles it into aTransform<Unit>
. What’s good about this is the calls toTransform.Compute(c)
andTransform.Print(p)
will also be compiling the scripts.Now, that’s the raw type side of it done. We need to implement an interpreter to run the
Transform<A>
DSL.First off we need some state that will be managed by the interpreter as its running:
It has three fields:
TotalAssets
, etc.)Now the
InterpreterState
type is defined we need to implement the interpreter itself:This is just a big
switch
statement, note howReturn
andFail
don’t callInterpret
like all the others.Interpret
is a recursive function that gets passed the result of the last operation so it can run its current operation. This walks theTransform<A>
DSL - essentially running it.There are three functions called
Compute
,Print
, andFilterRows
. They look like this:Note how
ExprCompute
andSumCompute
both callSetVar
to save the computed result.ExprCompute
doesn’t actually need to compute anything because it’s already been done by the compiledTransform<A>
andSumCompute
doesn’t need to do any filtering because it’s already done as part of theFilterRows
setup in theTransform
factory functions.And, that’s it, you can now run it using:
It outputs:
@imranypatel Because I see this as a useful example for people learning this library I have decided to make it into an example project (and because it’s probably hard to follow when everything is in one gist). It currently resides in the
Samples
folder on theasync-refactor
branchI have taken the idea a bit further than your initial requirements. It’s now possible to avoid the manual building of the operations followed by ‘loading’ them into the
Transform
. Now you can just use the scripts:The way I’ve achieved that is to support function invocation and variable assignment in the scripting system. The functions
sum
andfilter
are provided in aScriptFunctions
class that allows you to extend the set of available operations without much hassle:Below is the test method for loading from a script:
Note, these functions and scripting extensions are available in the version where you manually build the operations. Although, you only really need the
Print
operation now: