We use cookies and other tracking technologies to improve your browsing experience on our site, analyze site traffic, and understand where our audience is coming from. To find out more, please read our privacy policy.

By choosing 'I Accept', you consent to our use of cookies and other tracking technologies.

We use cookies and other tracking technologies to improve your browsing experience on our site, analyze site traffic, and understand where our audience is coming from. To find out more, please read our privacy policy.

By choosing 'I Accept', you consent to our use of cookies and other tracking technologies. Less

We use cookies and other tracking technologies... More

Login or register
to apply for this job!

Login or register to start contributing with an article!

Login or register
to see more jobs from this company!

Login or register
to boost this post!

Show some love to the author of this blog by giving their post some rocket fuel 🚀.

Login or register to search for your ideal job!

Login or register to start working on this issue!

Engineers who find a new job through Functional Works average a 15% increase in salary 🚀

Blog hero image

Building a large, complex web application with Haskell

Eugene Naumenko 30 November, 2019 (7 min read)

Building a large, complex web application with Haskell

There are many questions and doubts in the Intrenet if Haskell is "production ready", yet if it is "production ready" for rich browser-based web applications. People often argue about this while having only theoretical opinions, which is understandable - one can’t just build a large application for test purposes. And small test applications, like TodoMVC, tend not to give reliable answers.

We are happenning to build a web app using Haskell, which currently consists of 40K LOC of our own code (plus 100+ of the great Haskell libraries from Hackage). So finally I can share the real hands-on experience on building large web apps in Haskell using GHCJS.

Is 40K LOC a big application? Out of top 100 Github Haskell repos sorted by stars it would be on the 11th place. The biggest Purescript app (Slamdata) is around 55K LOC. Comparable app in Javascript would have 60-70K LOC probably, which is a reasonaby large application too.


Our app is yellow; as you can see, most apps are to the left of it (less LOC).


One thing worth mentioning is that just Haskell itself (or any other language) is not enough for building large, complex applications. Architectural vision becomes more and more important with project’s size growth.

Sadly, even Haskell programmers often underestimate real apps' complexity, and do not respect engineering foundations and principles when trying to build them. This ends up with a failure, obviously, which is blamed on Haskell, or functional programming, or OOP etc. Yet the real reason is because one can’t just do complex apps using naive approaches, like single global state, static composition etc.

Our application is built using locally-stateful components, that is components which encapsulate their state and expose /interfaces/ for communication with other components. There is no single state as in "Elm architecture" and other redux-es. It’s more like many small Elm architectures composed in the required way.

Inside the components do not use Elm architecture either. They are FRP-based, and might implement any topology of FRP networks depending on a given task, while Elm architecture is just one of these tolopogies.

The same architecture could be implemented without FRP, using state machines and reactive connections, or whatever. FRP just happens to be almost perfect fit for this architecture.

So, given scalable architecture, what can Haskell add to the equation for successful large application?


The type system shines on many levels:

  • types allow and encourage abstractions and /abstractions-based thinking/. With types abstractions can be written down explicitly, and code can be abstracted up or down by just specifying different types;
  • types are the ultimate /design tool/, allowing you to sculpt business logic as if using a clay, while ensuring it is correct and deterministic. As a bonus you don’t have to write so much tests;
  • types provide and ensure clear boundaries and /interfaces/, effectively dividing code into small self-sufficient parts. In fact, while the app is large and complex we barely feel this due to component architecture backed by types;
  • types can be used to disallow representing wrong program states, so, for example, we have no exceptions except for boundary layer (network IO);
  • even simple types allow you to go quite far (we’ve introduced reader monad transformer into the stack at 34K LOC);
  • types bring joy to refactoring. And refactoring in Haskell is not like refactoring in your average language – renaming some functions, etc. It’s more like scratching half of your code and rebuilding it back easily, as Gabriel Gonzalez said in a recent podcast;
  • and also types give that feeling when you are doing web development for 10 months and had not opened a browser dev console and debug tools a single time – because there are no runtime errors, and no debugging needed - if it compiles, it works. Well, except when integrating external 3rd-party code like maps, rich text editor, external APIs etc.

At the same time, the app is not some isolated type castle in it’s own world. /Au contraire/, the app has all kinds of stuff one would expect to see in a decent real-life web application:

  • integration with a lot of 3rd-party code and APIs: Highcharts, Leaflet maps, TinyMCE, Dropbox API, Google Drive API, Instagram API;
  • native APIs spport: drag-n-drop, svg, file upload, CORS, canvas;
  • interactive SVG and canvas graphics;
  • different app profiles and functionality based on logged in user’s role and permissions (impossible without higer order FRP);
  • users with proper permissions can switch to other user’s profiles (again, higer order FRP to the rescue);
  • complex forms, with multilevel modal popups, dynamic fields, wizard UX, undo (lots of business state);
  • access control lists (lots of context state);
  • batch jobs and progress tracking for them (external state real-time updates);
  • error reporting (more context state).

Code reuse

Another effect of using Haskell is very high degree of code reuse. Widgets and components are highly abstract, highly reusable, highly composable and, best of all, they emerge naturally from Haskell abstractions (functors, profunctors etc). code structure

code structure

The picture shows imports graph in the application. You can clearly see how modules are reused between the backend and the frontend.

Another kind of code reuse is code sharing between the backend and the frontend. Our backend is written in Haskell, obviously. This lets us have high conceptual integrity between server and client, and also we currently have 60 files with 4813 LOC shared between the backend and the frontend.

In fact, both backend and frontend are the same codebase, which is compiled using 2 different compilers (GHC and GHCJS) and results in 2 distinct apps - one runs on a server, another in a browser. This also renders impossible a scenario when data formats or APIs changes breake things. The frontend would just not compile if there are breaking changes in the backend, and vice versa.

Another benefit is that transport protocol and serialisation format are abstracted naturally. We just use command and query functions operating on native Haskell types on both ends, with the only difference from ordinary functions being that they are network-tolerant.

Runtime and concurrency

A code generated by GHCJS is different from code generated by most other *-to-js compilers. It is basically a bytecode for a stack machine, not very human-friendly. It is still debuggable though, but in a more low-level form, much like good old assembly level debugging.

GHCJS provides RTS, implemented in Javascript, which, in turn, provides standard Haskell concurrency primitives to a programmer and implements own scheduler to manage them internally. Heap is stored in Javascript arrays. And since it is's /the same/ GHC, just with a different backend, you can use advanced GHC extensions, benefit from optimisations of intermediate code, use hints for inlining etc. Things unprecedented in Javascript world :-)

This also means that you work with a simple concurrency model and tools - threads, STM etc. Asynchrony ("callbacks") is an implementation detail, as it should be. In the similar manner as with browser dev tools, you can develop a web app for months and see no callbacks, with the exception of FFI.

Speaking of FFI, it is easy and nice, arguably nicer that that of Purescript. Javascript literals are not merely strings concatenated into Javascript output code, but are parsed, checked, and occasionally refuse to compile indicating programmer errors. For example:

{-# LANGUAGE JavaScriptFFI      #-}
{-# LANGUAGE OverloadedStrings  #-}

import qualified GHCJS.Types    as T
import qualified GHCJS.Foreign  as F

foreign import javascript unsafe \"window.onload = $1\" onload :: T.JSFun a -> IO ()
foreign import javascript unsafe \"alert($1)\" alert :: T.JSString -> IO ()

Or even easier using quasi quotes:

{-# LANGUAGE QuasiQuotes #-}

import Prelude hiding (log)
import GHCJS.Foreign.QQ

log :: String -> IO ()
log msg = [js_| console.log(`msg); |]

delay :: Int -> IO ()
delay ms = [jsi_| setTimeout($c, `ms); |]

plus :: Int -> Int -> Int
plus x y = [js'| `x + `y |]

One can use external file for Javascript code and have it’s functions imported into haskell by names using /foreign import/.

Other nice things

You can use Template Haskell. It is compiled to Javascript and runs at compile time using nodejs. Having stack and a single simple config file is such a big relief after Javascript’s build tools madness. Even better, GHCJS could be installed and set up by stack using only stack.yaml config. For example, to start GHCJS project all you need is add something like this to your stack.yaml file:

compiler: ghcjs-
compiler-check: match-exact
       url: "http://.../ghcjs-"

Of course you can use Hayoo, Hackage, and look up functions by type signatures etc - again, unprecedented in Javascript world. Haddock is another unique thing in Javascript world - it builds documentation from source code (from comments), but also adds links to definitions to all types and functions of your code /and/ all other code, so you can click-and-go-to-definition on everything - this effectively replaces IDE go-to-definition feature, and gives a bird’s eye view of all the code used in your project.

IDE setup

You can use all the tools and plugins available for Haskell in different editors/IDEs, with few exceptions: ghci and ghc-mod do not work currently for GHCJS projects. More info on setting up Atom or VSCode for Haskell development can be found in my other blog post.


There is little GHCJS-specific documentation about the ways RTS and base libraries work. You can always read the source code at Github though.

Build time when building from scratch is quite long, more than 10 minutes. Incremental builds are much better: successful builds are about 80-180 sec in average, failed builds are about 5-10 sec.

The size of the resulting javscript file can be quite large. But Closure compiler and gzip help reduce it to acceptable size. Or, newer versions of GHCJS are expected to have new code generator, which would reduce output size up to 40%. The very minimal GHCJS-based app is about 300Kb, with all the overhead of RTS etc - these days it’s nothing :-)

With regards to FRP, it is sometimes hard to come up with right FRP patterns and constraints - it’s just too flexible. Also, proper disposal of higher order FRP components is somewhat tricky. It depends on FRP library implementation.requires extra thinking sometimes, but also helps at other times.


So far Haskell seems to work great for web development. Both for server and client parts. Highly recemmended :-)

The painting depicts a sunrise of Haskell above misty web development while brave programmers are sailing towards better programs under it's light.

Originally published on traversable.one