Haskell released two long awaited GHC compiler features on March 10th: the WebAssembly backend and the Javascript backend. GHC is now capable of generating code that runs in web browsers without the need for any additional tools.

The integration of these backends into GHC is great news for developers and businesses using Haskell for web development. It should enable significant advancements in the Haskell ecosystem. Hopefully, it will position Haskell as a solid option for both backend and frontend development.

I have attempted to use Haskell for front-end development multiple times and have been frustrated on each occasion. Despite having some great frameworks such as miso and reflex, the tooling has always been a challenge if you’re not using the standard linux/amd64 platform. In my case, I switch between macOS, windows and linux. A non-trivial amount of time needs to be invested in creating all the necessary boilerplate to use the language across different operating systems. Using Elm was the easy choice in some of the occasions.

There are two great blog posts to understand the behind-the-scenes work: one from IO, and one from Tweag.

Setting up Haskell for frontend development is still not trivial. Below, I’ve outlined what I’ve done so far to make it work for me on an M1 Mac.

Compiling the GHC Javascript backend Link to heading

Currently, the only way to experiment with the GHC Javascript backend is by using a custom build of GHC. GHC is not yet multi-target, so the compiler will have a pre-defined target. ghcup and other distribution channels provide binaries for the host system’s platform. Fortunately, compiling GHC is not that difficult.

In order to compile GHC, there are dependencies for compiling GHC itself, and dependencies which are specific for each of the backends (Javascript and wasm).

# Install common dependencies for GHC
brew install autoconf automake python ghcup

# A working version of GHC is required, it needs to be 9.2 or newer
ghcup install ghc 9.6.1
ghcup set ghc 9.6.1

# Alex and happy can be installed from the ghcup cabal
cabal update
cabal install alex happy

# Install dependencies for the Javascript backend
brew install emscripten node 

# Clone GHC repo locally
git clone --recurse-submodules https://gitlab.haskell.org/ghc/ghc.git

# Ensure you are in the ghc source tree
cd ghc

# Boot and configure the build process (see the comments below)
./boot && emconfigure ./configure --target=javascript-unknown-ghcjs

# Once all the dependencies are met, the compilation process should succeed
# The build took 17m48s on a "Macbook Pro M1 Pro" device to complete

# Build GHC
hadrian/build -j12 --flavour=quick --bignum=native --docs=none

The output of the emconfigure command on macOS should be similar to the one below. Note the version of the GHC is going to be whatever is on master branch, in the time of writing it is 9.9.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

----------------------------------------------------------------------
Configure completed successfully.

   Building GHC version  : 9.9.20230622
          Git commit id  : 4e1de71cd561d39fbc6bf95a62045b918761e077

   Build platform        : aarch64-apple-darwin
   Host platform         : aarch64-apple-darwin
   Target platform       : javascript-unknown-ghcjs

   Bootstrapping using   : /Users/amelo/.ghcup/bin/ghc
      which is version   : 9.6.1
      with threaded RTS? : YES

   Using (for bootstrapping) : gcc
   Using clang               : /opt/homebrew/Cellar/emscripten/3.1.41/libexec/emcc
      which is version       : 17.0.0
      linker options         : 
   Building a cross compiler : YES
   Unregisterised            : NO
   TablesNextToCode          : YES
   Build GMP in tree         : NO
   hs-cpp       : /opt/homebrew/Cellar/emscripten/3.1.41/libexec/emcc
   hs-cpp-flags : -E -undef -traditional -Wno-invalid-pp-token -Wno-unicode -Wno-trigraphs
   ar           : /opt/homebrew/Cellar/emscripten/3.1.41/libexec/emar
   ld           : /opt/homebrew/Cellar/emscripten/3.1.41/libexec/emcc
   nm           : /opt/homebrew/Cellar/emscripten/3.1.41/libexec/llvm/bin/llvm-nm
   objdump      : /usr/bin/objdump
   ranlib       : /opt/homebrew/Cellar/emscripten/3.1.41/libexec/emranlib
   otool        : otool
   install_name_tool : install_name_tool
   windres      : 
   dllwrap      : 
   genlib       : 
   Happy        : /Users/amelo/.cabal/bin/happy (1.20.0)
   Alex         : /Users/amelo/.cabal/bin/alex (3.2.7.1)
   sphinx-build : /opt/homebrew/opt/sphinx-doc/bin/sphinx-build
   xelatex      : 
   makeinfo     : 
   git          : /usr/bin/git
   cabal-install : /Users/amelo/.ghcup/bin/cabal

   Using optional dependencies:
      libnuma : NO
      libzstd : NO
         statically linked? : NO
      libdw   : NO

   Using LLVM tools
      clang : clang
      llc   : llc
      opt   : opt

   HsColour was not found; documentation will not contain source links

   Tools to build Sphinx HTML documentation available: YES
   Tools to build Sphinx PDF documentation available: NO
   Tools to build Sphinx INFO documentation available: NO
----------------------------------------------------------------------

Using the GHC Javascript backend Link to heading

There are two examples to start with. The first is the simplest Hello World application which outputs a text to Console. Create the file below, and, free free to name it HelloJS.hs. Check the official documentation for more details.

1
2
3
4
-- HelloJS.hs
module Main where
main :: IO ()
main = putStrLn "Hello, JavaScript!"

And use the compiled GHC to generate the Javascript code:

# Compiling the example using the just-built GHC
./_build/stage1/bin/javascript-unknown-ghcjs-ghc HelloJS.hs

# Execute the Javascript using nodeJS
node HelloJS.jsexe/all.js
# Output is "Hello, JavaScript!"

The example below creates a program with a basic DOM manipulation to insert a text to the HTML document that can be loaded from a browser.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
-- HelloDOM.hs
module Main where

import GHC.JS.Prim (JSVal, toJSString)

foreign import javascript "((message) => console.log(message))"
  consoleLog :: JSVal -> IO ()

foreign import javascript "((tagName) => document.createElement(tagName))"
  createElement :: JSVal -> IO JSVal

foreign import javascript "((data) => document.createTextNode(data))"
  createTextNode :: JSVal -> IO JSVal

foreign import javascript "((node, aChild) => node.appendChild(aChild))"
  appendChild :: JSVal -> JSVal -> IO JSVal

foreign import javascript "(() => document)"
  document :: IO JSVal

foreign import javascript "(() => document.body)"
  body :: IO JSVal

foreign import javascript "((message) => alert(message))"
  alert :: JSVal -> IO ()

main :: IO ()
main = do
    -- Creates a text element and adds it to the document
    container <- createElement (toJSString "div")
    text <- createTextNode (toJSString "Hello World")

    _ <- appendChild container text

    documentBody <- body
    _ <- appendChild documentBody container

    -- Shows the created node using Console
    consoleLog container
    

The output also generates a index.html file which can be accessed via a browser.

# Compiling the example using the just-built GHC
./_build/stage1/bin/javascript-unknown-ghcjs-ghc HelloDOM.hs

# Serve page
python3 -m http.server -d HelloDOM.jsexe/

# Open a brower on port 8000
open http://localhost:8000

Using the GHC WebAssembly backend Link to heading

I didn’t manage to compile a native binary of the WebAssembly backend yet, however, I created a docker image that can be used for experimenting the backend.

You can give it a go by doing:

# Create a simple example
echo 'main = putStrLn "hello world"' > hello.hs

# Compile to WebAssembly using docker
docker run -it --platform linux/amd64 \
	-v `pwd`:/app adrianomelo/ghc-wasm:latest \
	/usr/local/bin/wasm32-wasi-ghc/bin/wasm32-wasi-ghc \
	/app/hello.hs

# Execute the output file using `wasmtime`
wasmtime hello.wasm 
# Output: "Hello World"

Conclusion Link to heading

It is still early days for Haskell in the frontend. There are many things to be improved until it can comparable to mainstream languages. The two new features are a big step for the entire ecosystem. I will be following its steps closely and hope to share some of the findings and knowledge here.

Cheers!