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
.
----------------------------------------------------------------------
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.
|
|
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.
|
|
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!