Skip to content

Instantly share code, notes, and snippets.

@sgoguen
Last active November 2, 2022 13:23
Show Gist options
  • Save sgoguen/91209a28171a4f601209818971402244 to your computer and use it in GitHub Desktop.
Save sgoguen/91209a28171a4f601209818971402244 to your computer and use it in GitHub Desktop.
A Small Elm-like DSL in F#

Making Toys with F# - A Small Elm-like DSL in F#

A Small Elm-Like DSL in F#

I've been working on a talk about the virtues of building toy examples for the purpose of communicating ideas with simple interactive examples.

The toys I talk about in my presentation are based my interest in tools that allow programmers to quickly build web applications that allow them to explore their architecture. So to kickstart this series off, I want to introduce a simple component we can use to build other ideas.

Encoding HTML

HTML tags can be encoded simply with F# if we limit ourselves to basic tag features.

  1. A tag name (Let's ignore namespaces)
  2. A set of attributes that are basically key-value pairs
  3. A list of subtags or text.

We can encode it like this:

type TagName = string
type Attributes = Map<string, string>

type HtmlTag = 
  | HtmlTag of Name:TagName * Attributes:Attributes * Body:Tag list
  | HtmlText of string

If we wanted to make a page, an example page might look like this:

let myFanPage =
    HtmlTag("div", Map.empty, [
        HtmlTag("h1", Map.empty, [HtmlText("My Taylor Swift Fan Page")])
        HtmlTag("p", Map.empty, [HtmlText("She's simply amazing...")])
    ])

Cleaning up our F# Flavored Html

Right off the bat, we can agree this example is not only a little ugly, it's verbose. While I'd love to be able to use an HTML like syntax in my F# ala React's JSX, I don't see that happening anytime soon.

However, we can make our document look a little like Elm with a few helpful functions:

let makeTag name attributes body = 
    Tag(name, (Map.ofList attributes), body)    
    
let div = makeTag "div"
let h1 = makeTag "h1"
let p = makeTag "p"

Adding this little bit of code allows use to change our Taylor Swift fan page to look much more respectful:

let myFanPage =
    div [] [
        h1 [] [Text("My Taylor Swift Fan Page")]
        p [] [Text("She's simply amazing...")]
    ]

Now that's something we post with pride.

Using Partial Application to Create Specialized Functions

What's important to note is the makeTag function takes three parameters, but our example only shows us passing in one parameter when we called it in our example. What gives?

In F#, we can call a multiparameter function with less than the expected parameters. When we do that, we get back a new function that expects the remaining parameters. So in our example, we create three specialized functions for creating HTML tags with each one specialized for each tag.

That feature is called partial application, because we are providing a partial list of arguments to our function rather than all of them.

Printing our Page

All this is nice, but we have a fan page to publish so we're going to need some code to convert our fan page to HTML.

First, we'll need a few helper functions to encode string into proper HTML.

module Encoders =
    open System.Web
    let inline html(s:string) = HttpUtility.HtmlEncode(s)
    let inline attribute(s:string) = HttpUtility.HtmlAttributeEncode(s)
    let inline url(s:string) = HttpUtility.UrlEncode(s)

Next, I want a simple toy function that lets me write my HTML object to a TextWriter class.

let writeToTextWriter (w:TextWriter) (t:Tag) =
    match t with
    | Text(s) -> 
        w.Write("<span>")
        w.WriteLine(Encoders.html(s))
        w.Write("</span>")        
    | Tag(name, attributes, body) -> 
        w.Write(sprintf "<%s" name)
        //  Print the attributes
        for (name,value) in attributes |> Map.toList do
            w.Write(sprintf " %s=\"%s\"" (Encoders.attribute name) (Encoders.attribute value))
        w.WriteLine(">")
        //  Print the body of our tag
        for child in body do
            child |> writeToTextWriter w
        w.WriteLine(sprintf "</%s>" name)

Finally, if we want to convert our Tag into a string, we can write a method against our Tag type like so:

type Tag with
    override this.ToString() = 
        use sw = new StringWriter()
        this.Write(sw)
        sw.ToString()

That should be enough to get us started. If we were to take our example and run it:

let myFanPage =
    div [] [
        h1 [] [Text("My Taylor Swift Fan Page")]
        p [] [Text("She's simply amazing...")]
    ]

printfn(myFanPage.ToString())

We would expect an output that loosely resembles:

<div>
  <h1>My Taylor Swift Fan Page</h1>
  <p>She's simply amazing...</p>
</div>

There's nothing particularly exciting about this example, but stay tuned because I'm going to show you how we use this as a building block to create some useful
components.

// ******************** 1. Define out HtmlTag Type ******************
type TagName = string
type Attributes = Map<string, string>
type HtmlTag =
| HtmlTag of Name:TagName * Attributes:Attributes * Body:HtmlTag list
| HtmlText of string
// ******************** 2. A Simple Page to Test It ******************
let myFanPage1 =
HtmlTag("div", Map.empty,
[
HtmlTag("h1", Map.empty, [HtmlText("My Taylor Swift Fan Page")])
HtmlTag("p", Map.empty, [HtmlText("She's simply amazing...")])
])
// *************** 3. Let's create a teeny tiny DSL ******************
// Can we even really call it a DSL?
let makeTag name attributes body =
HtmlTag(name, (Map.ofList attributes), body)
let div = makeTag "div"
let h1 = makeTag "h1"
let p = makeTag "p"
// *************** 4. Test out the cleaned up version ******************
let myFanPage =
div [] [
h1 [] [HtmlText("My Taylor Swift Fan Page")]
p [] [HtmlText("She's simply amazing...")]
]
// **************** 5. Let's add code to print it ************************
module Encoders = begin
open System.Web
let inline html(s:string) = HttpUtility.HtmlEncode(s)
let inline attribute(s:string) = HttpUtility.HtmlAttributeEncode(s)
let inline url(s:string) = HttpUtility.UrlEncode(s)
end
open System.IO
let rec writeToTextWriter (w:TextWriter) (t:HtmlTag) = begin
match t with
| HtmlText(s) ->
w.Write("<span>")
w.WriteLine(Encoders.html(s))
w.Write("</span>")
| HtmlTag(name, attributes, body) ->
w.Write(sprintf "<%s" name)
// Print the attributes
for (name,value) in attributes |> Map.toList do
w.Write(sprintf " %s=\"%s\"" (Encoders.attribute name) (Encoders.attribute value))
w.WriteLine(">")
// Print the body of our tag
for child in body do
child |> writeToTextWriter w
w.WriteLine(sprintf "</%s>" name)
end
let toString(t:HtmlTag) =
use sw = new StringWriter()
t |> writeToTextWriter sw
sw.ToString()
// ******************** Let's print it *************************
myFanPage |> toString |> printfn "%s"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment