The Greater Helsinki Area F# User Group - F# & Azure - FI EN

Domain-modelling, DSL-language

Domain-model means data model (=structure) for developers, to guide problem solving to some direction.

Domain Specific Language (DSL) refers in this practices context only to Embedded / Internal style language, where independent functional (combinator-)library will be integrated to part of F#-language. We won't go through Fowler's OO-based DSL-book.

The Phases of Modelling

Modelling can be divided to three phases

  1. Selection/modelling of the primitives
    • The basic items of the system.
    • Only the most essential information
  2. Modelling the composition
    • How to combine primitives so that they fit seamlessly together
    • Combinators
  3. Syntax
    • Decorations, so that pieces are nice to use.

1. & 2. Modelling the Primitives and the Composition

Functionalities (/contract) can be modelled as data or as computation. Regardless of this, the same tooling is in use.

The Tooling of Modelling

Modelling should be done using three available tools:

  • Tuples
    • "Product type", can be thought as AND-operation
  • Discriminated unions
    • "Sum type", can be thought as OR-operation
  • Functions
    • Transition between the states
    • Modelling of an operation

In theory this Boolean-algebra is enough to model everything in information technology. The cleanest domain is achieved using these (i.e. avoid object-oriented-structures), but in practice you sometimes want to use also these:

  • Record, if there is a lot of bundled data (e.g. "ERP-object")
  • List, if you don't want to list each single item
  • Discriminated union can sometimes afterwards be replaced with an interface, if all the members are not known, i.e. some kind of plug-in -architecture is wanted.

Domain-model Modelling (Contract as Data)

Let's take an example, simple stocks exchange trading.

How would you model the trades?

  • I want to buy 100 Microsoft stocks.
  • I want to buy 500 Nokia stocks.
  • I want to sell 300 Google stocks.

Primitives would be the single trades. Composition is combining those to some kind of abstract syntax tree.

Usually the best practice is to model functionality (commands, verbs) rather than basic-objects (nouns). The model could look for example like this:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
module ``OptionTrade case 1`` =

    //Primitives:
    type OptionTrade = 
    | Buy of string*decimal // Buy "MSFT" 100 (amount)
    | Sell of string*decimal
    //Composition combinators:
    | ContractUntil of System.DateTime*OptionTrade
    | ContractAfter of System.DateTime*OptionTrade
    | Combine of OptionTrade*OptionTrade //or OptionTrade list

Or like this:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
module ``OptionTrade case 2`` =

    //Primitives:
    type OperationKind = Buy | Sell
    type DateTimeKind = Until | After
    type OptionTrade = 
    | Operation of OperationKind*string*decimal
    //Composition combinators:
    | Contract of DateTimeKind*System.DateTime*OptionTrade
    | Combine of OptionTrade*OptionTrade

The main difference between these two is that when these are later used, how easy is it to access the parameter data:

 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: 
36: 
37: 
module ``OptionTrade case 1 usage`` =
    open ``OptionTrade case 1``

    let create = 
        Combine(
            Combine(
                Buy("MSFT",100m),
                Buy("NOK",500m)),
            Sell("GOOG",300m))

    let rec purge = 
        function
            | Buy (name,amount) -> "" //...
            | Sell (name,amount) -> "" //...
            | ContractUntil (dt,opt) -> 
                 if dt<=System.DateTime.Now then purge opt else "" //...
            | ContractAfter (dt,opt) ->
                 if dt>=System.DateTime.Now then purge opt else "" //...
            | Combine (a,b) -> purge a + purge b

module ``OptionTrade case 2 usage`` =
    open ``OptionTrade case 2``

    let create = 
        Combine(
            Combine(
                Operation(Buy,"MSFT",100m),
                Operation(Buy,"NOK",500m)),
            Operation(Sell,"GOOG",300m))

    let rec purge = 
        function
            | Operation (kind,name,amount) -> // you could use when or...
                "" // use common name and amount functionality and then 
                   // match kind with buy/sell when needed
            | Contract (dtk, dt,opt) -> "" // match dt with...purge(opt)
            | Combine (a,b) -> purge a + purge b

Usually the better solution is the model 2, if the parameters describe the same data content, like in here. Then the operation is easier to change from one form to other, e.g. Buy -> Sell. Rather make more types than few mega-types.

This is the easiest and most clean way to model, and this way you will get a clear data model. The downside is that operations are solid (/"hard-coded"), so you can't compose your own structures to do whatever, like a library: the functionality has to obey the data model functionality. Usually this is enough, when your goal is to build simple little system that has clear boundaries.

When C# code so often leads to object-mapping-code from type to another, comes F# Object Expressions to rescue, you can create new instances without boilerplate-code:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
type MyInvoice = { 
    Sum : decimal;
    Tax : decimal;
    //... 
    Property01 : string;
    Property02 : string;
    Property03 : string;
    //...
    Property99 : string;
}
let myInstance = { Sum=10m; Tax=10m; Property01="a"; Property02="b"; Property03="c"; Property99="zzz" }
// Don't have to define all copied propertis like in LINQ-select you would have to:
let myNewCopy = { myInstance with Property01="Hello!" }

Modelling Contract as Computation

For a normal program this is too heavy concept (if you aren't familiar with call-cc and Haskell-libraries), but one good option, if it is a must to try to construct some kind of custom work-flow/process/rule -engine or framework. The difference to the previous one is that the user can refer this as a library and make more custom composition-operations. In that case this can be thought as a combinator-library or a DSL-language.

The model may be modelled as an operation/function ('a -> M<'b>), where "a" is "the program state", i.e. against what is the combination rules made, and M describes some kind of capsule/monad (for example a list), and "b" is the result type. The capsule is optional and also "a" and "b" don't have to be generics, if more precise business-oriented types are possible to define. At first, let's give a name to the function, just to avoid drowning into arrows:

1: 
2: 
type OptionTrade<'a,'b> =
| Operation of ('a -> List<'b>)

Now you may create a function that compose the functionality, e.g. with list:

1: 
2: 
3: 
4: 
let combine (Operation f1) (Operation f2) = 
    Operation(fun a -> [f1(a); f2(a)])

// OptionTrade -> OptionTrade -> OptionTrade

and define yet another function, the execution itself:

1: 
let eval (Operation f) a = f(a)

The user may now create whatever kind of Operation-style functions, that implement the syntax (a -> List<b>) and these can be combined as the user likes. For example:

1: 
let buy name amount = Operation(fun a -> [(name, amount)])

Finally the user calls the execution of the program.

The code logic is not separately developed, but the library already executes the code logics. So the composition-functions in the source code form the abstract syntax tree:

1: 
2: 
3: 
4: 
5: 
let myCombination = 
    combine
        (buy "MSFT" 100m)
        (buy "NOK" 500m)
let myDone = eval myCombination "now!"

Here the code is overly generic. Sell-operation is left as an exercise. Practical code sample (where is also the sell-operation and a pile of others) can be downloaded from the internet, code samples of the book "Real-World Functional Programming", chapter 15.

Standard-operations

When different functionalities are combined (even between different libraries), is is often handy to use the familiar standard-operations like:

  • map ('T -> 'R) -> M<'T> -> M<'R>
  • bind ('T -> M<'R>) -> M<'T> -> M<'R>
  • return 'T -> M<'T>

For example, map would be like this (with one and two parameters):

1: 
2: 
3: 
4: 
//Map with one parameters, just basic composition:
let map f (Operation f1) = Operation(fun a -> f(f1(a)))
//Map with f having two parameters:
let map2 f (Operation f1) (Operation f2) = Operation(fun a -> f (f1 a) (f2 a))

Avoid side-effects. Operation functionality can be figured out from the type syntax.

3. Syntax

F# has multiple features to make your functionality more convenient to use: Custom operators, Builder-syntax, Quotations (like C# -expression-tree), Query-syntax, ... Let's take a look at the first two:

Custom Operators

You can make your own operators, like a normal function, but writing the operator to parenthesis. When you use operator, the first parameter will come before the operator. For example this "funny trick":

1: 
2: 
let (+) x y = x-y
let minusOne = 5+6

...but this will have really nice and useful applications in custom DSL-languages, for example the previous combine-function (we don't need to list parameters due to partial application):

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
let (&) = combine

let myCombination2 = 
    buy "MSFT" 100m &
    buy "NOK" 500m &
    sell "GOOG" 300m

let myDone2 = eval myCombination2 "now!"

Overloading operators can be done also as usual member-functions to types. In F# you can also do extension methods (and extension properties!):

1: 
2: 
3: 
type System.String with
    member x.yell = x + "!"
// "hello".yell

Builder-syntax

You may build your own computational expressions: You just have to pick your state/side-effect that you want to encapsulate. Inside this the programming is with syntax myContext{ ... } where "myContext" is almost any word.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
// Return class like Async<T> or IEnumerable<T>:
type MyReturnClass(n:int) =
    member x.Value = n

type MyContextBuilder() =        
    member t.Return(x) = MyReturnClass(x)
    member t.Bind(x:MyReturnClass, rest) = 
        printfn "Binded %d" x.Value 
        rest(x.Value)

let context = MyContextBuilder()

let test =
    context{
        let! a = MyReturnClass(3) //"let!" calls builder's Bind
        let! b = MyReturnClass(5) //"let!" calls builder's Bind

        //Inside the monad you program like usual F#:
        let mult = a * b   
        let sum = mult + 1 

        return sum //"return" calls builder's Return(x)
    }
  • Inside the context, the bang (exclamation mark) -commands are ("syntactic sugar") calls to the corresponding methods of the builder-"interface".
  • "Interface" doesn't have to be fully covered, just the methods you like. The system is based on continuation: call-cc and reify.
  • Detailed description of the interface and how it works is available:

This can be used to capsulate some state, e.g. in stock-trading it could be the current bank account balance available.

Exercises

Exercise 1

This is some kind of fictional loan-application-process: pdf.

Here is a quick & dirty version of copy & pasting the texts to data types:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
type Terms = string
type Loan = 
| Offer of decimal*DateTime
| Approved of decimal*DateTime

type ``Loan application state`` =
| ``Status Received``
| ``Has Client`` of bool
| ``Has Credit`` of bool
| ``Offer terms are ok`` of Terms*bool
| ``Loan rejected``
| ``Manual offer terms`` of Terms
| ``Offer created`` of Terms*Loan
| ``Offer approved`` of Terms*Loan*bool
| ``Offer closed`` of Terms*Loan
| ``Money transferred`` of Loan
| ``Loan fully paid`` of bool*Loan
| ``Report created`` of Loan
  • How would you optimize these data types?
  • Write also a few fictional methods to use this process and transfer the loan application from a process state to another.
  • The data not involved to the process is better to encapsulate later as a new tuple beside than inside the process.

Exercise 2

Think what would be a bad domain-model?

Exercise 3

The chapter "Modelling Contract as Computation" did left the sell-operation away. Sell could either be a negation or a new custom type of type OperationKind = Buy | Sell.

Because there is only one sell-operation and combine-function takes several parameters, there is a little need for code changes, e.g. wrapper-operation to the list and maybe a custom operator for the wrapper:

1: 
let ``return`` (Operation f1) = Operation(fun a -> [f1(a)])

Implement the sell-operation and try to get it working in the original example.

This is how you can declare your own functions:

1: 
2: 
3: 
4: 
//Example use, with list-parameter, one parameters:
let doubleTradeAmount = map (fun al -> [fst(al |> List.head),snd(al |> List.head)*2m])
let goneDouble = doubleTradeAmount (buy "MSFT" 100m)
eval goneDouble ("not-used-initial-state",0m)

As you can see, the list-context is useless here and causes just extra List.head-calls. You can try to remove the list from the computations.

Create a custom function using map2-function.

Links / Sources

Tomas Petricek - Domain Specific Languages in F# - Video, Slides

Simon Peyton-Jones - Composing contracts: an adventure in financial engineering

Lab49 - The Algebra of Data, and the Calculus of Mutation

Philip Wadler - Theorems for free!

Back to the menu

type OptionTrade =
  | Buy of string * decimal
  | Sell of string * decimal
  | ContractUntil of DateTime * OptionTrade
  | ContractAfter of DateTime * OptionTrade
  | Combine of OptionTrade * OptionTrade

Full name: DomainModelEng.OptionTrade case 1.OptionTrade
union case OptionTrade.Buy: string * decimal -> OptionTrade
Multiple items
val string : value:'T -> string

Full name: Microsoft.FSharp.Core.Operators.string

--------------------
type string = System.String

Full name: Microsoft.FSharp.Core.string
Multiple items
val decimal : value:'T -> decimal (requires member op_Explicit)

Full name: Microsoft.FSharp.Core.Operators.decimal

--------------------
type decimal = System.Decimal

Full name: Microsoft.FSharp.Core.decimal

--------------------
type decimal<'Measure> = decimal

Full name: Microsoft.FSharp.Core.decimal<_>
union case OptionTrade.Sell: string * decimal -> OptionTrade
union case OptionTrade.ContractUntil: System.DateTime * OptionTrade -> OptionTrade
namespace System
Multiple items
type DateTime =
  struct
    new : ticks:int64 -> DateTime + 10 overloads
    member Add : value:TimeSpan -> DateTime
    member AddDays : value:float -> DateTime
    member AddHours : value:float -> DateTime
    member AddMilliseconds : value:float -> DateTime
    member AddMinutes : value:float -> DateTime
    member AddMonths : months:int -> DateTime
    member AddSeconds : value:float -> DateTime
    member AddTicks : value:int64 -> DateTime
    member AddYears : value:int -> DateTime
    ...
  end

Full name: System.DateTime

--------------------
System.DateTime()
   (+0 other overloads)
System.DateTime(ticks: int64) : unit
   (+0 other overloads)
System.DateTime(ticks: int64, kind: System.DateTimeKind) : unit
   (+0 other overloads)
System.DateTime(year: int, month: int, day: int) : unit
   (+0 other overloads)
System.DateTime(year: int, month: int, day: int, calendar: System.Globalization.Calendar) : unit
   (+0 other overloads)
System.DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int) : unit
   (+0 other overloads)
System.DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, kind: System.DateTimeKind) : unit
   (+0 other overloads)
System.DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, calendar: System.Globalization.Calendar) : unit
   (+0 other overloads)
System.DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int) : unit
   (+0 other overloads)
System.DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int, kind: System.DateTimeKind) : unit
   (+0 other overloads)
union case OptionTrade.ContractAfter: System.DateTime * OptionTrade -> OptionTrade
union case OptionTrade.Combine: OptionTrade * OptionTrade -> OptionTrade
type OperationKind =
  | Buy
  | Sell

Full name: DomainModelEng.OptionTrade case 2.OperationKind
union case OperationKind.Buy: OperationKind
union case OperationKind.Sell: OperationKind
type DateTimeKind =
  | Until
  | After

Full name: DomainModelEng.OptionTrade case 2.DateTimeKind
union case DateTimeKind.Until: DateTimeKind
union case DateTimeKind.After: DateTimeKind
type OptionTrade =
  | Operation of OperationKind * string * decimal
  | Contract of DateTimeKind * DateTime * OptionTrade
  | Combine of OptionTrade * OptionTrade

Full name: DomainModelEng.OptionTrade case 2.OptionTrade
union case OptionTrade.Operation: OperationKind * string * decimal -> OptionTrade
union case OptionTrade.Contract: DateTimeKind * System.DateTime * OptionTrade -> OptionTrade
val create : OptionTrade

Full name: DomainModelEng.OptionTrade case 1 usage.create
val purge : _arg1:OptionTrade -> string

Full name: DomainModelEng.OptionTrade case 1 usage.purge
val name : string
val amount : decimal
val dt : System.DateTime
val opt : OptionTrade
property System.DateTime.Now: System.DateTime
val a : OptionTrade
val b : OptionTrade
val create : OptionTrade

Full name: DomainModelEng.OptionTrade case 2 usage.create
val purge : _arg1:OptionTrade -> string

Full name: DomainModelEng.OptionTrade case 2 usage.purge
val kind : OperationKind
val dtk : DateTimeKind
type MyInvoice =
  {Sum: decimal;
   Tax: decimal;
   Property01: string;
   Property02: string;
   Property03: string;
   Property99: string;}

Full name: DomainModelEng.MyInvoice
MyInvoice.Sum: decimal
MyInvoice.Tax: decimal
MyInvoice.Property01: string
MyInvoice.Property02: string
MyInvoice.Property03: string
MyInvoice.Property99: string
val myInstance : MyInvoice

Full name: DomainModelEng.myInstance
val myNewCopy : MyInvoice

Full name: DomainModelEng.myNewCopy
type OptionTrade<'a,'b> = | Operation of ('a -> List<'b>)

Full name: DomainModelEng.OptionTrade<_,_>
union case OptionTrade.Operation: ('a -> List<'b>) -> OptionTrade<'a,'b>
Multiple items
module List

from Microsoft.FSharp.Collections

--------------------
type List<'T> =
  | ( [] )
  | ( :: ) of Head: 'T * Tail: 'T list
  interface IEnumerable
  interface IEnumerable<'T>
  member Head : 'T
  member IsEmpty : bool
  member Item : index:int -> 'T with get
  member Length : int
  member Tail : 'T list
  static member Cons : head:'T * tail:'T list -> 'T list
  static member Empty : 'T list

Full name: Microsoft.FSharp.Collections.List<_>
val combine : OptionTrade<'a,'b> -> OptionTrade<'a,'b> -> OptionTrade<'a,List<'b>>

Full name: DomainModelEng.combine
val f1 : ('a -> List<'b>)
val f2 : ('a -> List<'b>)
val a : 'a
val eval : OptionTrade<'a,'b> -> a:'a -> List<'b>

Full name: DomainModelEng.eval
val f : ('a -> List<'b>)
val buy : name:'a -> amount:'b -> OptionTrade<'c,('a * 'b)>

Full name: DomainModelEng.buy
val name : 'a
val amount : 'b
val a : 'c
val myCombination : OptionTrade<string,List<string * decimal>>

Full name: DomainModelEng.myCombination
val myDone : List<List<string * decimal>>

Full name: DomainModelEng.myDone
val map : f:(List<'a> -> List<'b>) -> OptionTrade<'c,'a> -> OptionTrade<'c,'b>

Full name: DomainModelEng.map
val f : (List<'a> -> List<'b>)
val f1 : ('c -> List<'a>)
val map2 : f:(List<'a> -> List<'b> -> List<'c>) -> OptionTrade<'d,'a> -> OptionTrade<'d,'b> -> OptionTrade<'d,'c>

Full name: DomainModelEng.map2
val f : (List<'a> -> List<'b> -> List<'c>)
val f1 : ('d -> List<'a>)
val f2 : ('d -> List<'b>)
val a : 'd
val x : int
val y : int
val minusOne : int

Full name: DomainModelEng.minusOne
val myCombination2 : OptionTrade<string,List<List<string * decimal>>>

Full name: DomainModelEng.myCombination2
val myDone2 : List<List<List<string * decimal>>>

Full name: DomainModelEng.myDone2
Multiple items
type String =
  new : value:char -> string + 7 overloads
  member Chars : int -> char
  member Clone : unit -> obj
  member CompareTo : value:obj -> int + 1 overload
  member Contains : value:string -> bool
  member CopyTo : sourceIndex:int * destination:char[] * destinationIndex:int * count:int -> unit
  member EndsWith : value:string -> bool + 2 overloads
  member Equals : obj:obj -> bool + 2 overloads
  member GetEnumerator : unit -> CharEnumerator
  member GetHashCode : unit -> int
  ...

Full name: System.String

--------------------
System.String(value: nativeptr<char>) : unit
System.String(value: nativeptr<sbyte>) : unit
System.String(value: char []) : unit
System.String(c: char, count: int) : unit
System.String(value: nativeptr<char>, startIndex: int, length: int) : unit
System.String(value: nativeptr<sbyte>, startIndex: int, length: int) : unit
System.String(value: char [], startIndex: int, length: int) : unit
System.String(value: nativeptr<sbyte>, startIndex: int, length: int, enc: System.Text.Encoding) : unit
val x : System.String
member System.String.yell : int

Full name: DomainModelEng.yell
Multiple items
type MyReturnClass =
  new : n:int -> MyReturnClass
  member Value : int

Full name: DomainModelEng.MyReturnClass

--------------------
new : n:int -> MyReturnClass
val n : int
Multiple items
val int : value:'T -> int (requires member op_Explicit)

Full name: Microsoft.FSharp.Core.Operators.int

--------------------
type int = int32

Full name: Microsoft.FSharp.Core.int

--------------------
type int<'Measure> = int

Full name: Microsoft.FSharp.Core.int<_>
val x : MyReturnClass
member MyReturnClass.Value : int

Full name: DomainModelEng.MyReturnClass.Value
Multiple items
type MyContextBuilder =
  new : unit -> MyContextBuilder
  member Bind : x:MyReturnClass * rest:(int -> 'a) -> 'a
  member Return : x:int -> MyReturnClass

Full name: DomainModelEng.MyContextBuilder

--------------------
new : unit -> MyContextBuilder
val t : MyContextBuilder
member MyContextBuilder.Return : x:int -> MyReturnClass

Full name: DomainModelEng.MyContextBuilder.Return
member MyContextBuilder.Bind : x:MyReturnClass * rest:(int -> 'a) -> 'a

Full name: DomainModelEng.MyContextBuilder.Bind
val rest : (int -> 'a)
val printfn : format:Printf.TextWriterFormat<'T> -> 'T

Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.printfn
property MyReturnClass.Value: int
val context : MyContextBuilder

Full name: DomainModelEng.context
val test : MyReturnClass

Full name: DomainModelEng.test
val a : int
val b : int
val mult : int
val sum : int
type Terms = string

Full name: DomainModelEng.Terms
type Loan =
  | Offer of decimal * obj
  | Approved of decimal * obj

Full name: DomainModelEng.Loan
union case Loan.Offer: decimal * obj -> Loan
union case Loan.Approved: decimal * obj -> Loan
type bool = System.Boolean

Full name: Microsoft.FSharp.Core.bool
val doubleTradeAmount : (OptionTrade<(string * decimal),(string * decimal)> -> OptionTrade<(string * decimal),(string * decimal)>)

Full name: DomainModelEng.doubleTradeAmount
val al : List<string * decimal>
val fst : tuple:('T1 * 'T2) -> 'T1

Full name: Microsoft.FSharp.Core.Operators.fst
val head : list:'T list -> 'T

Full name: Microsoft.FSharp.Collections.List.head
val snd : tuple:('T1 * 'T2) -> 'T2

Full name: Microsoft.FSharp.Core.Operators.snd
val goneDouble : OptionTrade<(string * decimal),(string * decimal)>

Full name: DomainModelEng.goneDouble

Creative Commons -copyright Tuomas Hietanen, 2014, thorium(at)iki.fi, Creative Commons