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

Domain-mallinnus, DSL-kieli

Domain-mallilla tarkoitetaan (kehittäjille suunnattua) tietomallia (=rakennetta), ohjaamaan ongelmanratkaisua tietyille raiteille.

Domain Specific Language (DSL) viittaa tässä vain Embedded / Internal tyyliseen kieleen, jossa itsenäinen funktionaalinen (kombinaattori-)kirjasto upotetaan osaksi F#-kieltä. Emme käy läpi Fowler:in OO-pohjaista DSL-kirjaa.

Mallinnuksen vaiheet

Mallinnus koostuu kolmesta vaiheesta

  1. Valitaan/mallinnetaan alkiot (primitives)
    • Järjestelmän perus-yksiköiden tilat.
    • Vain keskeisin/oleellisin tieto
  2. Mallinnetaan kompositio (yhdistäminen)
    • Miten primitiivejä yhdistellään niin, että ne sopivat saumattomasti yhteen
    • Kombinaattorit
  3. Syntaksi
    • Lisäkoristelut, jotta palikoita on kiva käyttää.

1. & 2. Mallinnetaan alkiot ja niiden kompositio

Toiminnallisuudet voidaan lähteä mallintamaan joko tietona (contract as data) tai laskentaoperaatioina (contract as computation). Riippumatta tästä, käytössä on samat työkalut.

Mallinnuksen työkalut

Mallinnus kannattaa tehdä käyttäen kolmea työkalua:

  • Tuplet
    • "Product type", voidaan ajatella AND-operaationa
  • Discriminated union
    • "Sum type", voidaan ajatella OR-operaationa
  • Funktiot
    • Siirtymä tilojen välillä
    • Toiminnon mallinnus

Teoriassa boolean-operaatiot riittävät tietotekniikassa mallintamaan kaiken. Selkein domain tuleekin käyttäen näitä (ts. lähtökohtaisesti vältä oliorakenteita), mutta käytännössä halutaan ehkä käyttää apuna vielä seuraavia:

  • Record, jos yhteen-niputettavaa tietoa on paljon (esim. "ERP-olio")
  • Lista, jos yksittäisiä kohteita ei haluta listata
  • Discriminated union voidaan jälkikäteen vaihtaa rajapinnaksi, jos kaikki sen jäsenet eivät ole tiedossa, ts. halutaan plugin-arkkitehtuuri.

Domain-mallin mallintaminen (contract as data)

Otetaan esimerkkinä yksinkertainen osake/pörssikauppa.

Kuinka mallinnetaan:

  • Haluan ostaa Microsoftin osakkeita 100kpl
  • Haluan ostaa Nokian osakkeita 500kpl
  • Haluan myydä Googlen osakkeita 300kpl

Alkiot (primitiivit) olisivat yksittäisiä kauppoja, ja kompositio on sitä, että primitiiveistä yhdistellään eräänlainen abstrakti syntaksipuu.

Usein kannattaa lähteä mallintamaan järjestelmän toimintoja (commandeja, verbejä), eikä niinkään "perus-olioita" (substantiiveja). Malli voisi näyttää esimerkiksi tältä:

 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

Tai tältä:

 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

Näiden kahden merkittävin ero on siinä, että sitten kun näitä käytetään, niin kuinka helposti päästään käsiksi parametritietoihin:

 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

Kannattaa valita vaihtoehto 2, jos käytettävät parametrit kuvaavat samaa tietosisältöä, kuten tässä. Silloin operaatio on helpompi muokata muodosta toiseen, esim. vaihtaa Buy -> Sell. Tee mieluummin useita tyyppejä kuin mega-tyyppejä.

Tämä on helpoin ja selkein tapa mallintaa, näin saat selkeän tietomallin. Haittapuolena on, että operaatiot ovat kiinteitä (/"kovakoodattu"), eli et voi kirjastomaisesti yhdistää omia rakenteita tekemään mitä tahansa, vaan toiminnallisuuden on kunnioitettava tietomallin toiminnallisuuksia. Usein tämä riittää, kun tavoitteena on rakentaa yksittäinen järjestelmää, jolla on selkeät rajat.

Siinä missä C# usein johtaa olio-mappaus-koodiin tyypistä toiseen, auttavat F# Object Expressionit, joilla voi luoda instansseja (structeista, olioista, interfaceista) ilman boilerplate-koodia:

 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!" }

Tietomallin mallintaminen laskentaoperaatioina (contract as computation)

Tämä on perus-ohjelmaan usein turhan monimutkainen konsepti (ellei call-cc ja Haskell-kirjastot ole entuudestaan tuttuja), mutta hyvä vaihtoehto, jos on pakko yrittää rakentaa jonkinlainen oma workflow/prosessi/sääntö -moottori tai framework. Erona edelliseen on se, että käyttäjä voi referoida tämän kirjastona ja itse tehdä komposito-operaatioita lisää. Tällöin kyseessä alkaa olla jo kombinaattorikirjasto tai DSL-kieli.

Tietomallia voidaan mallintaa toimintona/funktiona ('a -> M<'b>), jossa a on "ohjelman tila", ts. mitä vastaan kombinaatiosääntöjä tehdään, ja M kuvaa jonkinlaista kapselia/monadia (vaikkapa lista), ja b taas on tuloksen tyyppi. Kapseli on vapaaehtoinen ja myöskään a ja b ei tarvitse olla geneerisiä, jos tarkemmat tyypit on business-mielessä mahdollista määrittää. Aluksi annetaan funktiolle nimi, jotta ei hukuta nuoliin:

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

Nyt voidaan luoda funktio, joka yhdistää kaksi toiminnallisuutta, esim. listalla:

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

// OptionTrade -> OptionTrade -> OptionTrade

ja määritellään vielä toinen funktio, itse suoritus:

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

Nyt käyttäjä voi tehdä mitä tahansa Operation-tyylisiä funktioita, jotka toteuttavat syntaksin (a -> List<b>) ja näitä voidaan yhdistellä käyttäjän mielen mukaan. Esim:

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

Lopuksi käyttäjä pyytää suorittamaan ajon.

Koodin logiikkaa ei erikseen tehdä domainin rinnalle, vaan kirjasto jo suorittaa koodin logiikan. Eli abstrakti-syntaksi-puu muodostuukin kooste-funktioista lähdekoodissa:

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

Tässä tehtiin koodista turhan geneeristä ja myynti-(sell)-operaatio jätetään harjoitustehtäväksi. Käytännön tarkempi lähdekoodiesimerkki (jossa myös myynti on toteutettu, ja kasa muita operaatioita) on saatavilla netistä, kirjan "Real-World Functional Programming" koodiesimerkeistä, kappale 15.

Standardi-operaatiot

Yhdisteltäessä eri toiminnallisuuksia (jopa eri kirjastojen välillä), on usein kätevää käyttää tuttuja standardi-operaatiota kuten esim:

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

Esim map menisi näin (yhdellä ja kahdella parametrilla):

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))

Vältä sivuvaikutuksia. Operaation tietotyypistä voi jo päätellä mitä itse operaatio tekee.

3. Syntaksi

Jotta toiminnallisuuksiasi olisi parempi käyttää, niin F#:ssa on lukuisia hienouksia: omat custom-operaattorit, Builder-syntaksi, Quotations (kuin C# -expression-puu), Query-syntaksi, ... Tässä esiteltynä tarkemmin kaksi:

Custom-operaattorit

F#:ssa voit tehdä omat operaattorisi, kuten normaali funktio, mutta kirjoittamalla operaattorin sulkuihin. Käytettäessä operaattorin ensimmäinen parametri tulee ennen operaattoria. Esimerkkinä seuraava "hauska jekku":

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

mutta tälle on myös erittäin mukavia käyttötarkoituksia omissa DSL-kielissä, tästä esimerkkinä edellisen kohda combine-funktio (partial application:in takia ei tarvitse listata parametreja):

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!"

Ylikuormitettavat operaattorit voi myös määrittää tavallisina member-funktioina tyypille. F#:ssa voit tehdä myös extension-metodeja (ja extension-propertyjä!):

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

Builder-syntaksi

Omia computational expressioneita (/monadeita) voi rakentaa lennosta: Keksit vain tilan/sivuvaikutuksen, jonka kapseloit. Tämän sisällä ohjelmoidaan konteksti{ ... } -syntaksilla (jossa "konteksti" voi olla melkein mikä tahansa valitsemasi sana).

 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)
    }
  • Kontekstin sisällä huutomerkki-käskyt ("syntaktisokeria") ohjaavat builder-"rajapinnan" vastaaviin metodeihin.
  • "Rajapinnasta" ei tarvitse täyttää kuin valitsemasi metodit. Homma perustuu continuationiin (tarkemmin: call-cc) ja reify:yn.
  • Tarkempi kuvaus rajapinnan metodeista ja sisäisestä toiminnasta löytyy netistä.

Tätä voidaan käyttää kapseloimaan jokin asiaan liittyvä tila, osakekauppaesimerkissä se voisi olla vaikkapa pankkitilin rahamäärä, joka on käytössä kauppoihin.

Harjoitustehtävät

Harjoitustehtävä 1

Ohessa on piirretty kuvitteellinen lainanhakuprosessi: pdf.

Siitä copy & pastella on hutaistu tekstit oheisiin tietotyyppeihin:

 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
  • Miten optimoisit näitä tietotyyppejä?
  • Kirjoita myös muutama kuvitteellinen metodi käyttämään tätä kuvattua prosessia ja kuljettamaan lainahakemusta prosessin vaiheesta toiseen.
  • Prosessiin kuulumaton data kannattaa kapseloida mieluummin uutena tuplena myöhemmin rinnalle, kuin prosessin sisään.

Harjoitustehtävä 2

Mieti mikä olisi huono domain-malli?

Harjoitustehtävä 3

Kappaleessa "Tietomallin mallintaminen laskentaoperaatioina" oli jätetty myynti-operaatio pois. Myynti voisi olla joko vain negaatio, tai sitten lisätään taas rinnalle oma type OperationKind = Buy | Sell.

Koska myyntejä on vain yksi, ja combine ottaa kaksi parametria, niin tarvitaan vähän koodimuutoksia, esim. wrapperi käytettyyn listaan, ja ehkä tälle oma custom-operaattori:

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

Toteuta myyntioperaatio ja koita saada se toimimaan alkuperäisessä esimerkissä.

Tässä esimerkki miten tehdään omia funktioita:

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)

Kuten huomataan, niin tässä kontekstissa turhasta listakapselista on nyt vaivaa (turha List.head). Voit koittaa ottaa listan pois.

Tee jokin oma operaatio, joka käyttää map2-funktiota.

Linkit / Lähteet

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!

Takaisin valikkoon

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

Full name: DomainModelFin.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: DomainModelFin.OptionTrade case 2.OperationKind
union case OperationKind.Buy: OperationKind
union case OperationKind.Sell: OperationKind
type DateTimeKind =
  | Until
  | After

Full name: DomainModelFin.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: DomainModelFin.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: DomainModelFin.OptionTrade case 1 usage.create
val purge : _arg1:OptionTrade -> string

Full name: DomainModelFin.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: DomainModelFin.OptionTrade case 2 usage.create
val purge : _arg1:OptionTrade -> string

Full name: DomainModelFin.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: DomainModelFin.MyInvoice
MyInvoice.Sum: decimal
MyInvoice.Tax: decimal
MyInvoice.Property01: string
MyInvoice.Property02: string
MyInvoice.Property03: string
MyInvoice.Property99: string
val myInstance : MyInvoice

Full name: DomainModelFin.myInstance
val myNewCopy : MyInvoice

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

Full name: DomainModelFin.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: DomainModelFin.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: DomainModelFin.eval
val f : ('a -> List<'b>)
val buy : name:'a -> amount:'b -> OptionTrade<'c,('a * 'b)>

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

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

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

Full name: DomainModelFin.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: DomainModelFin.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: DomainModelFin.minusOne
val myCombination2 : OptionTrade<string,List<List<string * decimal>>>

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

Full name: DomainModelFin.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: DomainModelFin.yell
Multiple items
type MyReturnClass =
  new : n:int -> MyReturnClass
  member Value : int

Full name: DomainModelFin.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: DomainModelFin.MyReturnClass.Value
Multiple items
type MyContextBuilder =
  new : unit -> MyContextBuilder
  member Bind : x:MyReturnClass * rest:(int -> 'a) -> 'a
  member Return : x:int -> MyReturnClass

Full name: DomainModelFin.MyContextBuilder

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

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

Full name: DomainModelFin.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: DomainModelFin.context
val test : MyReturnClass

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

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

Full name: DomainModelFin.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: DomainModelFin.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: DomainModelFin.goneDouble

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