DSLs for HTML / XML with F# Combinator Library

How to write "Domain Specific Language" in F# to generate HTML/XML files. These DSLs will be implemented using "Discriminated Unions" and a set of functions that will form a "Combinators Library". Similar approaches are used in FParsec, WebSharper, Suave, financial contract description and other popular libraries.

What is a DSL?

A domain specific language (DSL): is a programming language tailored for a particular application domain, which captures precisely the semantics of the application domain.

What is a Combinator Library?

A combinatory library offers functions (the combinators) that combine functions together to make bigger functions. These kinds of libraries are particularly useful for allowing domain-specific programming languages to be easily embedded into a general purpose.

HTML DSL

For the following F# expression:

    let timesTable n =
    html []
        [
        head [] [title [] %(sprintf "%d Times table" n)]
        body [] 
            [
            h1 [] %(sprintf "%d Times table" n)
            ul [] 
                [
                for i in 1..12 -> 
                    li [] 
                    %(sprintf "%d x %d = %d" n i (n*i))]
            ]
        ]
    timesTable 17 |> HtmlToString

we expect this output:

    <html>
        <head>
            <title>17 Times table</title>
        </head>
        <body>
            <h1>17 Times table</h1>
            <ul>
                <li>17 x 1 = 17</li>
                <li>17 x 2 = 34</li>
                ...
                <li>17 x 11 = 187</li>
                <li>17 x 12 = 204</li>
            </ul>
        </body>
    </html>

CSV Type Provider and XML DSL

For the following F# use of CSV Type Provider and XML DSL:

    [<Literal>]
    let filePath = __SOURCE_DIRECTORY__+ @"\msft.csv"
    type Stocks = CsvProvider<filePath>
    let msft = Stocks.Load(filePath)

    xml []
    [ 
        for row in msft.Rows ->
        value 
            [
            date (row.Date.ToString("yyyyMMdd"))
            high (string(row.High.ToString("N2")))
            ]
            %(string(row.Close.ToString("N2")))
    ]
    |> XmlToString

we expect these output:

    <xml>
     <value Date="20170216" High="65.24">64.52</value>
     <value Date="20170215" High="64.57">64.53</value>
     <value Date="20170214" High="64.72">64.57</value>
     <value Date="20170213" High="64.86">64.72</value>
     ...
     <value Date="19860317" High="29.75">29.50</value>
     <value Date="19860314" High="29.50">29.00</value>
     <value Date="19860313" High="29.25">28.00</value>
    </xml>

HTML sample and data tree structure

Here is a pedagocial HTML sample to build the HTML DSL:

    <html xmlns="http://www.w3.org/1999/xhtm">
        <head>
            <title>GDNUG Talk</title>
        </head>
        <body>
            <h1>Hello world!</h1>
            <form id="form1">
                <input type="text" name="name" />
            </form>
        </body>
    </html>

From these sample we see the data tree structure of HTML.

    - Tag: html
        - Attr: xmlns "http://www.w3.org/1999/xhtm"
        - Tag: head
            - Tag: title
                - Text: GDNUG Talk
        - Tag: body
            - Tag: h1
                - Text: Hello world!
            - Tag: form
                - Attr: id form1
                - Tag: input
                    -Attr: type text
                    -Attr: name name

We need to be able to generate and manipulate this tree in an abstract, type safe way

Discrimated Unions

From the MSDN page for Discriminated Unions:

Discriminated unions provide support for values that can be one of a number of named cases, possibly each with different values and types.

Used in particular for :

  • object hierarchies
  • tree data structures

Discrimated Unions for HTML

    type Html =
        | Text of string
        | Attr of string * string
        | Tag of string * Html list * Html list

The cases Text, Attr and Tag provide:

  • functions to create the different cases
  • pattern matching

First version of HTML sample:

    Tag("html", 
        [Attr("xmlns", "http://www.w3.org/1999/xhtm")], 
        [
            Tag("head", 
                [], 
                [
                    Tag("title", 
                        [], 
                        [Text("GDNUG Talk")])
                ])
            Tag("body", 
                [], 
                [
                    Tag("h1", 
                        [], 
                        [Text("Hello world!")])
                    Tag("form", 
                        [Attr("id", "form1")],
                        [Tag("input", 
                            [
                            Attr("type", "text")
                            Attr("name", "name")], 
                            [])
                        ])
                ])
        ])

HtmlToString function

The more complex function in this article is the function HtmlToString that render the HTML Discriminated Unions to string.

These function uses:

  • Pattern matching
  • Local functions
  • Mutable state with a StringBuilder
  • Recursivity
let HtmlToString (html:Html) : string =
    let stringBuilder = new StringBuilder()

    let rec toString (html:Html) (indent:int) : unit =
        let spaces = String.replicate indent " "
        let newLine = System.Environment.NewLine
        match html with
            | Text(txt) -> stringBuilder.Append(txt) |> ignore
            | Attr(name, value) -> stringBuilder.Append(sprintf " %s=\"%s\"" name value) |> ignore
            | Tag(tag, attrs, [Text s]) ->
                stringBuilder.Append(sprintf "%s<%s" spaces tag) |> ignore
                for attr in attrs do
                    toString attr indent
                stringBuilder.Append(sprintf ">%s</%s>%s" s tag newLine) |> ignore
            | Tag(tag, attrs, []) ->
                stringBuilder.Append(sprintf "%s<%s" spaces tag) |> ignore
                for attr in attrs do
                    toString attr indent
                stringBuilder.Append(sprintf "/>%s" newLine) |> ignore
                ()
            | Tag(tag, attrs, tags) ->
                stringBuilder.Append(sprintf "%s<%s" spaces tag) |> ignore
                for attr in attrs do
                    toString attr indent
                stringBuilder.Append(sprintf ">%s" newLine) |> ignore
                for tag in tags do
                    toString tag (indent + 4)
                stringBuilder.Append(sprintf "%s</%s>%s" spaces tag newLine) |> ignore
                ()
            
    do toString html 0
    stringBuilder.ToString()

Combinator Library

Function to simplify the creation of Html element :

    let tag name attrs elems = Tag(name, attrs, elems)
    let (%=) name value = Attr(name,value)
    let (~%) s = [Text(s.ToString())]

Second version of HTML sample:

    tag "html" 
        ["xmlns"%="http://www.w3.org/1999/xhtm"] 
        [
            tag "head" 
                [] 
                [
                    tag "title" 
                        [] 
                        %("GDNUG Talk")
                ]
            tag "body" 
                [] 
                [
                    tag "h1" 
                        [] 
                        %("Hello world!")
                    tag "form" 
                        ["id"%="form1"]
                        [tag "input" 
                            [
                            "type"%="text"
                            "name"%="name"] 
                            []
                        ]
                ]
        ]

DSL functions :

    let html attrs elems = tag "html" attrs elems
    let head attrs elems = tag "head" attrs elems
    let title attrs elems = tag "title" attrs elems
    let body attrs elems = tag "body" attrs elems
    let h1 attrs elems = tag "h1" attrs elems
    let form attrs elems = tag "form" attrs elems  
    let input attrs elems = tag "input" attrs elems  

Third and final version of HTML sample:

    html 
        ["xmlns"%="http://www.w3.org/1999/xhtm"] 
        [
            head 
                [] 
                [
                    title 
                        [] 
                        %("GDNUG Talk")
                ]
            body 
                [] 
                [
                    h1 
                        [] 
                        %("Hello world!")
                    form 
                        ["id"%="form1"]
                        [input 
                            [
                            "type"%="text"
                            "name"%="name"] 
                            []
                        ]
                ]
        ]

With this version we can write the abobe timesTable function.

Question: How to do the same in POO language?

XML DSL

We can write a variant of the DSL for XML:

type Xml =
    | Text of string
    | Attr of string * string
    | Elem of string * Xml list * Xml list

let XmlToString xml : string =
    let stringBuilder = new StringBuilder()

    let rec toString xml indent : unit =
        let spaces = String.replicate indent " "
        let newLine = System.Environment.NewLine
        match xml with
            | Text(txt) -> stringBuilder.Append(txt) |> ignore
            | Attr(name, value) -> stringBuilder.Append(sprintf " %s=\"%s\"" name value) |> ignore
            | Elem(tag, attrs, [Text s]) ->
                stringBuilder.Append(sprintf "%s<%s" spaces tag) |> ignore
                for attr in attrs do
                    toString attr indent
                stringBuilder.Append(sprintf ">%s</%s>%s" s tag newLine) |> ignore
            | Elem(tag, attrs, []) ->
                stringBuilder.Append(sprintf "%s<%s" spaces tag) |> ignore
                for attr in attrs do
                    toString attr indent
                stringBuilder.Append(sprintf "/>%s" newLine) |> ignore
                ()
            | Elem(tag, attrs, elems) ->
                stringBuilder.Append(sprintf "%s<%s" spaces tag) |> ignore
                for attr in attrs do
                    toString attr indent
                stringBuilder.Append(sprintf ">%s" newLine) |> ignore
                for elem in elems do
                    toString elem (indent + 1)
                stringBuilder.Append(sprintf "%s</%s>%s" spaces tag newLine) |> ignore
                ()
            
    do toString xml 0
    stringBuilder.ToString()

let elem name attrs elems = Elem(name, attrs, elems)
let (~%) s = [Text(s.ToString())]
let (%=) name value = Attr(name,value)
let xml = elem "xml"
let value = elem "value"
let date value = Attr("Date", value)
let high value = Attr("High", value)

With this DSL we can write the CSV to XML code from the introduction.

Quotes on functional programming

Combinator Library products

  • FParsec: a parser combinator library for F#.
  • WebSharper: a fundamentally different web framework for developing functional and reactive .NET applications
  • Suave: a simple web development F# library providing a set of combinators to manipulate route flow and task composition.
  • Composing contracts: an adventure in financial engineering: a fundamentally different web framework for developing functional and reactive .NET applications