DSLs for HTML / XML with F# Combinator Library

- This post is part of the Geneva .Net User Group Meetup on Tuesday, February 21, 2017
- The sources are available on GitHub
- Slides
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
- To understand recursion, you must first understand recursion.
- It is better to have 100 functions operate on one data structure than 10 functions on 10 data structures. Alan Perlis
- A methodology of programming [...] we first define a data structure, the abstract syntax, then a convenient way to produce by CAML values of this type... Pierre Weis, Xavier Leroy, The CAML language
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