Writing HTML apps in Haskell - Part 2

In part 1 I described the basic two-process setup for accessing the DOM from within Haskell.

In this instalment I will walk through several other crucial ingredients for a proper Haskell HTML app.

Motivation

First, here’s a GIF of an animated DOM rendered in Haskell that can be modified with developer tools.

Serving local content

Browsers usually fetch data form the network via HTTP or one of its successors (http2, quic). The URL looks like http://localhost/resource. WebKit supports registering custom URL schemes so we don’t have to hit the network and run a server. We instead register a haskell:// schema with a IO () callback which gets executed each time the rendering engine loads a resource.

It’s important to register this URI schema before creating an actual render view but the code itself is pretty simple:

webContextRegisterUriScheme context "haskell" $ \request -> do
  uri <- uRISchemeRequestGetUri request
  print uri -- prints e.g. "haskell://hello.html"
  -- [omitted]
-- [omitted]
webViewLoadUri view "haskell://hello.html"

A virtual DOM

Relative to JavaScript speeds DOM updates in browsers are slow. That’s why all modern frameworks try to minimise DOM access. The most common technique is to keep a DOM-like data structure in memory. After changes we diff that DOM-like structure against its previous version, and generate a set of “patches” that then modify the real, in-browser DOM. Find a longer and nicer description here.

I adapted some code from Bodil Stokke to implement a virtual DOM.

The code for the GIF above can now be written like this:

testVNode :: Int -> VNode
testVNode n =
  GE.div_ [onClick printCoordinates, GA.class_ "container"]
  [ GE.h1_ [] [GE.text_ "Hello"]
  , GE.div_ []
    [ GE.button_ [GA.class_ "btn btn-primary"]
      [GE.text_ (show n)]
    ]
  ]

And at some later point you call patch to blast the virtual tree from testVNode into the real DOM:

patch api body oldDom newDom

CSS

CSS preprocessors are another important component of modern web development. These preprocessors like less and sass take away some of the pain of CSS. They allow e.g. defining variables for common colours, or take care of writing the correct browser-specific prefixes for some CSS properties.

I added a simple sass reader to the example code that uses hsass to render the bootstrap V4 sass code.

Including the sass file works as usual, except that we’re using the haskell:// scheme, and that hsass is re-processing on every reload.

<link rel="stylesheet" type="text/css" href="haskell://bootstrap.css">

Other notes

  • The current code can be found here.
  • GObject and also WebKit require running code that modifies the DOM in the GObject main loop. So you can use all of Haskell’s runtime but DOM modifications have to be run via idleAdd.
  • Building the separate-process plugin requires manual linking against the Haskell runtime, e.g with -lHSrts_thr_l-ghc8.0.1. Note the _thr and the _l. Those two mean that we’re linking against the runtime that supports threading and event logging. The normal -threaded and -eventlog flags don’t work for some reason.
  • I want to use hpack but it doesn’t support enough Cabal features yet.
  • The compile & link times are crushing.
  • The function names are auto-camel-cased from the GTK names which often leads to weird capitalisation, e.g. uri_scheme_request_get_uri becomes uRISchemeRequestGetUri.

What next?

I’m going to clean up the code to make it more like a library with a few entry points. If someone with stack-knowledge wanted to stackify it I’d be eager to help.

I’m surprised how well this works. Writing a UI to render some home automation data took me less than an hour. I hope I can convince others to join in to make this a more generally useful project!