Getting Along with JavaScript
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:
versions.json
{
"reflex-platform": {
"owner": "reflex-frp",
"repo": "reflex-platform",
"branch": "develop",
"rev": "",
"sha256": ""
}
}
I can then update this by running:
$ ./updater versions.json reflex-platform
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'
(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 {
inherit sha256;
url = "https://github.com/${owner}/${repo}/tarball/${rev}";
};
reflex-platform = fetcher (builtins.fromJSON (builtins.readFile ./versions.json)).reflex-platform;
in (import reflex-platform { system = builtins.currentSystem; }).project ({ pkgs, ... }: {
useWarp = true;
withHoogle = 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 tojsaddle-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:
$ nix-shell -A shells.ghc
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
main = mainWidget $ el "div" $ do
t <- inputElement def
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:
$ nix-shell -A shells.ghc --run 'ghcid -T "Main.main" --command "cabal new-repl"'
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
main = mainWidget $ el "div" $ do
t <- textArea def
el "div" $
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
main = mainWidgetWithHead widgetHead $ el "div" $ do
t <- textArea def
el "div" $
dynText $ _textArea_value t
where
widgetHead :: DomBuilder t m => m ()
widgetHead = do
script "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 src = elAttr "script" ("type" =: "text/javascript" <> "src" =: src) 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) {
var viz = new Viz();
.renderSVGElement(string)
viz.then(function(element) {
.innerHTML = element.outerHTML;
e
}).catch(function(error) {
.innerHTML = error;
e
}) }
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 ()
viz element string = do
call vizJs vizJs [element, string]
pure ()
vizJs :: JSM JSVal
vizJs = eval
"(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)
import Viz (viz)
main = mainWidgetWithHead widgetHead $ el "div" $ do
t <- textArea def
e <- _element_raw . fst <$> el' "div" blank
performEvent_ $ ffor (updated (_textArea_value t)) $ \text -> liftJSM $ do
jsE <- toJSVal e
jsT <- toJSVal text
viz jsE jsT
where
widgetHead :: DomBuilder t m => m ()
widgetHead = do
script "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 src = elAttr "script" ("type" =: "text/javascript" <> "src" =: src) 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 ()
viz element string = do
viz <- new (jsg "Viz") ()
render <- viz # "renderSVGElement" $ [string]
result <- render # "then" $ [(fun $ \_ _ [e] -> do
outer <- e ! "outerHTML"
element <# "innerHTML" $ outer
)]
result # "catch" $ [(fun $ \_ _ [err] ->
element <# "innerHTML" $ err
)]
pure ()
(revision)
This is recognisably the same logic as before, using some new JSaddle operators:
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 ()
viz element string = do
viz <- new (jsg "Viz") ()
render <- viz ^. js1 "renderSVGElement" string
result <- render ^. js1 "then" (fun $ \_ _ [e] -> do
outer <- e ! "outerHTML"
element ^. jss "innerHTML" outer)
result ^. js1 "catch" (fun $ \_ _ [err] ->
element ^. jss "innerHTML" err)
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:
$ nix-build -A ghcjs.small-viz
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>
<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 {
inherit sha256;
url = "https://github.com/${owner}/${repo}/tarball/${rev}";
};
reflex-platform = fetcher (builtins.fromJSON (builtins.readFile ./versions.json)).reflex-platform;
pkgs = (import reflex-platform {}).nixpkgs;
project = import ./default.nix;
html = pkgs.writeText "index.html" ''
<!DOCTYPE html>
<html>
<head>
<script language="javascript" src="all.js"></script>
</head>
<body>
</body>
</html>
'';
in pkgs.runCommand "glitch" {} ''
mkdir -p $out
cp ${html} $out/index.html
cp ${project.ghcjs.small-viz}/bin/small-viz.jsexe/all.js $out/all.js
''
(revision)
And then produce the files we need to copy over with:
$ nix-build glitch.nix
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 {
inherit sha256;
url = "https://github.com/${owner}/${repo}/tarball/${rev}";
};
reflex-platform = fetcher (builtins.fromJSON (builtins.readFile ./versions.json)).reflex-platform;
pkgs = (import reflex-platform {}).nixpkgs;
project = import ./default.nix;
html = pkgs.writeText "index.html" ''
<!DOCTYPE html>
<html>
<head>
<script language="javascript" src="all.js"></script>
</head>
<body>
</body>
</html>
'';
in pkgs.runCommand "glitch" {} ''
mkdir -p $out
cp ${html} $out/index.html
${pkgs.closurecompiler}/bin/closure-compiler \
--externs=${project.ghcjs.small-viz}/bin/small-viz.jsexe/all.js.externs \
--jscomp_off=checkVars \
--js_output_file="$out/all.js" \
-O ADVANCED \
-W QUIET \
${project.ghcjs.small-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
cabal-version: >=1.10
-- Initial package description 'small-viz.cabal' generated by 'cabal init'.
-- For further documentation, see http://haskell.org/cabal/users-guide/
name: small-viz
version: 0.1.0.0
-- synopsis:
-- description:
-- bug-reports:
license: BSD3
license-file: LICENSE
author: Vaibhav Sagar
maintainer: vaibhavsagar@gmail.com
-- copyright:
-- category:
build-type: Simple
extra-source-files: CHANGELOG.md
executable small-viz
main-is: Main.hs
other-modules: Viz
-- other-extensions:
build-depends: base >=4.12 && <4.13
, lens
, jsaddle
, reflex
, reflex-dom
-- hs-source-dirs:
default-language: Haskell2010
if impl(ghcjs)
ghc-options: -dedupe
(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.