Getting Along with JavaScript

Posted on 29 October 2019

For the last couple of weeks, I’ve been obsessed with the idea of running Haskell in the browser. I know this is possible, because this is what I do at work every day, but the applications I work on professionally are complex beasts with Haskell backends and dedicated servers making them available to users. I’m looking for something lighter that I can serve statically using GitHub Pages or Glitch, so I can plop some code on a webpage and never worry about hosting ever again.

My first instinct was to reach for a tool like Obelisk, which bills itself as “an easy way to develop and deploy your Reflex project”. Although it does work as advertised(!), it is geared towards the needs of the large apps I mentioned above. It prerenders webpages where possible to make projects as snappy as possible, works best within the confines of the Obelisk libraries, and assumes at least one NixOS target that will host your website, all of which mean it doesn’t yet scale down to my comparatively modest needs. It is possible to use Obelisk anyway, but I found myself using too few of its features to justify the effort, and I decided to move down a level and use Reflex Platform directly, which is a set of changes and overrides to a revision of Nixpkgs to best support building full-stack and mobile Haskell applications.

If you’d like to follow along, I have the code available at this gist with each revision representing a step in the progression.

Setting up reflex-platform

I like to use the updater script described in a previous blog post, so I’ll start by copying that over and creating a versions.json with the following contents:

I can then update this by running:

to get the latest reflex-platform . At the time of writing, this is the revision I used:

pinned versions.json { "reflex-platform" : { "owner" : "reflex-frp" , "repo" : "reflex-platform" , "branch" : "develop" , "rev" : "8f4b8973a06f78c7aaf1a222f8f8443cd934569f" , "sha256" : "167smg7dyvg5yf1wn9bx6yxvazlk0qk64rzgm2kfzn9mx873s0vp" } }

(revision)

Creating a project skeleton

The next step is to get a Haskell project skeleton in place. I used cabal init for this as follows:

$ nix-shell -p ghc cabal-install --run 'cabal init -lBSD3' -p ghc cabal-install --run

(revision)

which generated an executable-only project, just like I wanted. I named this project small-viz , because it’s a small project using the Viz.js library, but more on that later.

The next step is to actually use reflex-platform to develop this project, for which we need to write a little Nix. Here’s the default.nix I used:

default.nix let # ./updater versions.json reflex-platform fetcher = { owner, repo, rev, sha256, ... }: builtins.fetchTarball { = { owner, repo, rev, sha256, ... }: inherit sha256 ; sha256 url = "https://github.com/ ${owner} / ${repo} /tarball/ ${rev} " ; }; reflex-platform = fetcher (builtins.fromJSON (builtins.readFile ./versions.json)) .reflex-platform ; = fetcher (builtins.fromJSON (builtins.readFile ./versions.json)) in ( import reflex-platform { system = builtins.currentSystem ; } ) .project ({ pkgs, ... }: { reflex-platform { system = builtins.currentSystem({ pkgs, ... }: useWarp = true ; = true withHoogle = false ; = false packages = { = { small-viz = ./. ; = ./. } ; shells = { = { ghc = [ "small-viz" ] ; = [ ghcjs = [ "small-viz" ] ; = [ }; })

(revision)

This sets up our project to build with both GHC and GHCJS, because we want to develop with GHC but eventually use GHCJS to create our final artifact. I also set a few more options:

useWarp = true changes the JSaddle backend to jsaddle-warp so we can develop using the browser, as described here. withHoogle = false means we don’t build a local Hoogle database every time our packages are updated, because this step is slow and I never used the local documentation anyway.

For the next step I’ll assume binary cache substitution has been set up as described here:

This should download a lot (and build almost nothing from source since we are pulling from the cache), and then enter a shell environment with our dependencies in scope.

Starting our Reflex app

Now we can start developing our Reflex app! We can start from the small example described here:

Main.hs {-# LANGUAGE OverloadedStrings #-} import Reflex.Dom = mainWidget $ el "div" $ do mainmainWidgetel t <- inputElement def inputElement def $ _inputElement_value t dynText_inputElement_value t

(revision)

We also have to add reflex-dom and reflex to our dependencies in our .cabal file, and then we can get a automatically-reloading development build with one command:

This allows a native Haskell process to control a web page, so we can navigate to it using our browser at http://localhost:3003 and have a fast feedback loop. In practice there is a lot of browser refreshing involved, but this is still much nicer than having to do a GHCJS build each time we want to look at our changes. Now we have an input box that repeats what we type into it, which is a good start. I should point out that this works a lot better on Google Chrome (or Chromium) than it does on Firefox, and that’s what I’ll be using for development. The final GHCJS output does not have this limitation.

So where are we going with this? My plan is to build a crude version of the Viz.js homepage, where you can write DOT and see it rendered instantly. Viz.js is the result of compiling the venerable Graphviz to JavaScript using Emscripten. It’s no longer maintained but still works fine as far as I can tell. In order to do this I want to use some kind of JavaScript FFI to call out to viz.js , but first I want to swap out our text input for a text area, and move the repeated output to just below the text area instead of beside it.

Main.hs {-# LANGUAGE OverloadedStrings #-} import Reflex.Dom = mainWidget $ el "div" $ do mainmainWidgetel t <- textArea def textArea def "div" $ el $ _textArea_value t dynText_textArea_value t

(revision)

Integrating with Viz.js

The latest version of Viz.js is available here, and we can include it using mainWidgetWithHead :

Main.hs {-# LANGUAGE OverloadedStrings #-} import Reflex.Dom = mainWidgetWithHead widgetHead $ el "div" $ do mainmainWidgetWithHead widgetHeadel t <- textArea def textArea def "div" $ el $ _textArea_value t dynText_textArea_value t where widgetHead :: DomBuilder t m => m () t mm () = do widgetHead "https://cdn.jsdelivr.net/npm/viz.js@2.1.2/viz.min.js" script "https://cdn.jsdelivr.net/npm/viz.js@2.1.2/full.render.min.js" script = elAttr "script" ( "type" =: "text/javascript" <> "src" =: src) blank script srcelAttrsrc) blank

(revision)

Now we can poke around with our browser developer tools until we have a useful JavaScript function. Here’s what I came up with, based on the examples in the wiki:

function (e , string) { (estring) var viz = new Viz () ; viz() viz . renderSVGElement (string) (string) . then ( function (element) { (element) e . innerHTML = element . outerHTML ; } ) . catch ( function (error) { (error) e . innerHTML = error ; error } ) }

Then we can start thinking about how we want to do JavaScript interop! Although there is a GHCJS FFI as described in the wiki, this doesn’t seem to work at all with GHC, and that means we can’t use it during development. I don’t think that’s good enough, and fortunately we don’t have to settle for this and instead can use jsaddle , which describes itself as “an EDSL for calling JavaScript that can be used both from GHCJS and GHC”. We can add jsaddle to our dependencies, add Viz to the exposed-modules stanza in our .cabal file, and create a new module Viz , and then we can use the eval and call functions to call our JavaScript directly:

Viz.hs module Viz where import Language.Javascript.JSaddle viz :: JSVal -> JSVal -> JSM () () = do viz element string call vizJs vizJs [element, string] pure () () vizJs :: JSM JSVal = eval vizJseval "(function(e, string) { \ \ var viz = new Viz(); \ \ viz.renderSVGElement(string) \ \ .then(function(element) { \ \ e.innerHTML = element.outerHTML; \ \ }) \ \ .catch(function(error) { \ \ e.innerHTML = error; \ \ }) \ \})"

(revision)

JSaddle runs operations in JSM , which is similar to IO , and all functions take values of type JSVal that can be represented as JavaScript values. We pass vizJs to call twice because the second parameter represents the this keyword.

Wiring everything up together is just a few more lines of code:

Main.hs {-# LANGUAGE OverloadedStrings #-} import Reflex.Dom import Language.Javascript.JSaddle (liftJSM, toJSVal) (liftJSM, toJSVal) import Viz (viz) (viz) = mainWidgetWithHead widgetHead $ el "div" $ do mainmainWidgetWithHead widgetHeadel t <- textArea def textArea def e <- _element_raw . fst <$> el' "div" blank _element_rawel'blank $ ffor (updated (_textArea_value t)) $ \text -> liftJSM $ do performEvent_ffor (updated (_textArea_value t))\textliftJSM <- toJSVal e jsEtoJSVal e <- toJSVal text jsTtoJSVal text viz jsE jsT where widgetHead :: DomBuilder t m => m () t mm () = do widgetHead "https://cdn.jsdelivr.net/npm/viz.js@2.1.2/viz.min.js" script "https://cdn.jsdelivr.net/npm/viz.js@2.1.2/full.render.min.js" script = elAttr "script" ( "type" =: "text/javascript" <> "src" =: src) blank script srcelAttrsrc) blank

(revision)

There’s a lot going on here, so I’ll explain in a little more detail.

Instead of an element which displays the textarea contents as they are updated, we just want a reference to a blank <div> , so we use the el' function and pull out the raw element. performEvent_ mediates the interaction between Reflex and side-effecting actions, like our function that updates the DOM with a rendered graph, so we want to use it to render a new graph every time the textarea is updated.

An introduction to Reflex is out of scope for this blog post, but it’s worth mentioning that the textarea value is represented as a Dynamic , which can change over time and notify consumers when it has changed. This can be thought of as the combination of a related Behavior and Event . performEvent_ only takes an Event , and we can get the underlying Event out of a Dynamic with updated .

ffor is just flip fmap , and we use it to operate on the underlying Text value, convert both it and the reference to the element we want to update to JSVal s, and then pass them as arguments to the viz function we defined earlier. Now we should have a working GraphViz renderer in our browser!

Using the FFI better

We could stop here, but I think we can do better than evaluating JavaScript strings directly. JSaddle is an EDSL, which means we can rewrite our JavaScript in Haskell:

Viz.hs module Viz where import Language.Javascript.JSaddle viz :: JSVal -> JSVal -> JSM () () = do viz element string <- new (jsg "Viz" ) () viznew (jsg) () <- viz # "renderSVGElement" $ [string] renderviz[string] <- render # "then" $ [(fun $ \_ _ [e] -> do resultrender[(fun\_ _ [e] <- e ! "outerHTML" outer <# "innerHTML" $ outer elementouter )] # "catch" $ [(fun $ \_ _ [err] -> result[(fun\_ _ [err] <# "innerHTML" $ err elementerr )] pure () ()

(revision)

This is recognisably the same logic as before, using some new JSaddle operators:

# is for calling a JavaScript function

is for calling a JavaScript function ! is for property access

is for property access <# is a setter

Note also that all callables take a list of JSVal s as arguments, since JSaddle doesn’t know how many arguments we intend to pass in advance.

This is an improvement, but we can do even better using the lensy API (after adding lens to our dependencies):

Viz.hs module Viz where import Language.Javascript.JSaddle import Control.Lens ((^.)) ((^.)) viz :: JSVal -> JSVal -> JSM () () = do viz element string <- new (jsg "Viz" ) () viznew (jsg) () <- viz ^. js1 "renderSVGElement" string rendervizjs1string <- render ^. js1 "then" (fun $ \_ _ [e] -> do resultrenderjs1(fun\_ _ [e] <- e ! "outerHTML" outer ^. jss "innerHTML" outer) elementjssouter) ^. js1 "catch" (fun $ \_ _ [err] -> resultjs1(fun\_ _ [err] ^. jss "innerHTML" err) elementjsserr) pure () ()

(revision)

Again, not much has changed except that we can use convenience functions like js1 and jss .

I’m told that there is some overhead to using JSaddle which it’s possible to get rid of by using a library like ghcjs-dom , but I haven’t explored this approach and I will leave this as an exercise for the reader. If you learn how to do this, please teach me!

Now we are able to run Haskell on the frontend without having to write any JavaScript ourselves. The final step is to put this on the internet somewhere!

Deploying our app

Building with GHCJS is straightforward:

I’m enamoured of the idea of deploying this to Glitch, so let’s look into doing that. The index.html created by the default GHCJS build is unnecessary, and we can simplify it:

index.html <!DOCTYPE html > html <html> <head> <script language= "javascript" src= "all.js" ></script> </head> <body> </body> </html>

The only JavaScript file that needs to be copied over is then all.js . We can write a glitch.nix file to simplify this process:

glitch.nix let # ./updater versions.json reflex-platform fetcher = { owner, repo, rev, sha256, ... }: builtins.fetchTarball { = { owner, repo, rev, sha256, ... }: inherit sha256 ; sha256 url = "https://github.com/ ${owner} / ${repo} /tarball/ ${rev} " ; }; reflex-platform = fetcher (builtins.fromJSON (builtins.readFile ./versions.json)) .reflex-platform ; = fetcher (builtins.fromJSON (builtins.readFile ./versions.json)) pkgs = (import reflex-platform {}) .nixpkgs ; = (import reflex-platform {}) project = import ./default.nix ; = import ./default.nix html = pkgs.writeText "index.html" '' = pkgs.writeText < ! DOCTYPE html > html < html > < head > < script language= "javascript" src= "all.js" >< /script > language=src=/script < / head > < body > < / body > < / html > '' ; in pkgs.runCommand "glitch" {} '' {} mkdir -p $out -p cp ${html} $out /index.html /index.html cp ${project .ghcjs.small - viz } /bin/small-viz.jsexe/all.js $out /all.js viz/bin/small-viz.jsexe/all.js/all.js ''

(revision)

And then produce the files we need to copy over with:

I’ve gone ahead and done this, and it’s up on small-viz.glitch.me/.

Now that everything’s working, it would be nice to reduce the size of all.js , which is currently over 5MB. Obelisk uses the Closure Compiler to minify JavaScript, and we can adapt what it does and another example by Tom Smalley that I found when I was looking into this to update glitch.nix :

glitch.nix let # ./updater versions.json reflex-platform fetcher = { owner, repo, rev, sha256, ... }: builtins.fetchTarball { = { owner, repo, rev, sha256, ... }: inherit sha256 ; sha256 url = "https://github.com/ ${owner} / ${repo} /tarball/ ${rev} " ; }; reflex-platform = fetcher (builtins.fromJSON (builtins.readFile ./versions.json)) .reflex-platform ; = fetcher (builtins.fromJSON (builtins.readFile ./versions.json)) pkgs = (import reflex-platform {}) .nixpkgs ; = (import reflex-platform {}) project = import ./default.nix ; = import ./default.nix html = pkgs.writeText "index.html" '' = pkgs.writeText < ! DOCTYPE html > html < html > < head > < script language= "javascript" src= "all.js" >< /script > language=src=/script < / head > < body > < / body > < / html > '' ; in pkgs.runCommand "glitch" {} '' {} mkdir -p $out -p cp ${html} $out /index.html /index.html ${pkgs .closurecompiler } /bin/closure-compiler \ ${project .ghcjs.small - viz } /bin/small-viz.jsexe/all.js.externs \ --externs=viz/bin/small-viz.jsexe/all.js.externs \ --jscomp_off=checkVars \ " $out /all.js" \ --js_output_file= -O ADVANCED \ -W QUIET \ ${project .ghcjs.small - viz } /bin/small-viz.jsexe/all.js viz/bin/small-viz.jsexe/all.js ''

(revision)

And this brings the size down to under 2MB.

Tom Smalley points out that there is even a -dedupe flag that GHCJS accepts, and although I couldn’t find good documentation for this (beyond a Reddit post), it does get the filesize down to 1MB:

small-viz.cabal - version : >= 1.10 cabalversion -- Initial package description 'small-viz.cabal' generated by 'cabal init'. -- For further documentation, see http://haskell.org/cabal/users-guide/ : small - viz namesmallviz : 0.1 . 0.0 version -- synopsis: -- description: -- bug-reports: : BSD3 license - file : LICENSE licensefile : Vaibhav Sagar author : vaibhavsagar @ gmail . com maintainervaibhavsagargmailcom -- copyright: -- category: - type : Simple build - source - files : CHANGELOG.md extrasourcefilesCHANGELOG.md - viz executable smallviz - is : Main.hs mainisMain.hs - modules : Viz othermodules -- other-extensions: - depends : base >= 4.12 && < 4.13 builddependsbase , lens , jsaddle , reflex - dom , reflexdom -- hs-source-dirs: - language : Haskell2010 defaultlanguage if impl(ghcjs) impl(ghcjs) - options : - dedupe ghcoptionsdedupe

(revision)

I think this is a good stopping point. We’ve:

Built a frontend-only Reflex app Integrated with a JavaScript library Used the JSaddle FFI idiomatically Deployed to Glitch

and I hope I’ve convinced you to take a closer look at Haskell the next time you want to write something that runs in the browser.

Thanks to Ali Abrar, Farseen Abdul Salam, and Tom Smalley for comments and feedback.