Producer Consumer example using GHCJS and NixOS April 13, 2015

For a time I have wanted to try GHCJS, but it was rumored to be hard to install. However, as I recently installed NixOS on my desktop computer and it has packages for GHCJS, I decided I would give GHCJS a shot. Bas van Diijk had made a mailing list post outlining how he uses uses GHCJS with NixOS. However, being new to NixOS and the Nix package manager language, I had a hard time understanding Bas van Diijk’s post. But with time and a lot of errors, I got a working setup. In this post I will describe what I did.

If you want to follow this howto, you properly already have NixOS installed. If not, you can find a good guide in the Nix Manual. If you want to install NixOS in a virtual machine this guide will help you.

Our example will depend on the unstable Nix package repository. Therefore do:

> mkdir ProducerConsumer > cd ProducerConsumer > git clone https://github.com/NixOS/nixpkgs.git

Unfortunately, I could not get the code to work with the newest version of Nix unstable. This is not necessarily surprising as unstable is a moving and not always perfectly working target – hence the name. But here the Nix way comes to the rescue, as one can just roll back the Nix Git repository back to when it did work:

> cd nixpkgs > git reset --hard 1901f3fe77d24c0eef00f73f73c176fae3bcb44e > cd ..

So with Nix you can easily follow the bleeding edge, without getting traped in a non-working unstable branch. I would not know how to do this easily with my former Linux distribution Debian.

We will start creating the client:

> mkdir client > mkdir client/src

We need a client/default.nix file descriping how to buld this client:

{ pkgs ? (import <nixpkgs> {}) , hp ? pkgs.haskellPackages_ghcjs # use ghcjs packages instead of ghc packages }: hp.cabal.mkDerivation (self: { pname = "ProducerConsumerClient"; version = "1.0.0"; src = ./.; isLibrary = false; isExecutable = true; buildDepends = [ hp.ghcjsDom hp.random hp.stm ]; buildTools = [ hp.cabalInstall ]; })

This is fairly standard default.nix for Haskell projects, except that we are using GHCJS instead of GHC. If you’re not familiar with Nix expressions, then a good guide can be found here.

We also need a Cabal file client/ProducerConsumerClient.cabal:

name: ProducerConsumerClient version: 1.0.0 author: Mads Lindstrøm build-type: Simple cabal-version: >=1.10 executable producer-consumer-client main-is: Main.hs build-depends: base >=4.7 && <4.8, ghcjs-dom >= 0.1.1.3, random >= 1.0.1.3, stm >= 2.4.2 hs-source-dirs: src default-language: Haskell2010

Finally we need to actually program. We create a small example with a producer and consumer of integers. And a showNumbersVar function, which presents the numbers to the user. We only have one source file client/src/Main.hs:

module Main ( main ) where import GHCJS.DOM import GHCJS.DOM.Document import GHCJS.DOM.HTMLElement import System.Random (randomRIO) import Control.Concurrent.STM (TVar, retry, atomically, modifyTVar, readTVar, newTVar) import Control.Concurrent (threadDelay, forkIO) main :: IO () main = do numbersVar <- atomically $ newTVar [1, 2, 3] forkIO (producer numbersVar) forkIO (consumer numbersVar) showNumbersVar [] numbersVar showNumbersVar :: [Int] -> TVar [Int] -> IO () showNumbersVar lastNumbers numbersVar = do currentNumbers <- atomically (do numbers <- readTVar numbersVar if lastNumbers == numbers then retry else return numbers ) Just doc <- currentDocument Just body <- documentGetBody doc htmlElementSetInnerHTML body ("<h1>" ++ unlines (map (\x -> show x ++ "<br>") currentNumbers) ++ "</h1>") showNumbersVar currentNumbers numbersVar producer :: TVar [Int] -> IO () producer numbersVar = do sleepMillies 500 2000 newNumber <- randomRIO (0, 100) atomically (modifyTVar numbersVar (newNumber:)) producer numbersVar consumer :: TVar [Int] -> IO () consumer numbersVar = do sleepMillies 500 2000 atomically (modifyTVar numbersVar (drop 1)) consumer numbersVar sleepMillies :: Int -> Int -> IO() sleepMillies minMs maxMs = randomRIO (minMs*1000, maxMs*1000) >>= threadDelay

This is ordinary Haskell and the code should not have many surprises for the experienced Haskell programmer. It is very nice that we can use Software Transactional Memory (STM) to handle integer list. STM is likely to be especially helpful in a user interface application, where there necessarily is a lot of concurrency.

We can build the client now:

> nix-build -I . client

If successful you should get a link called result, which points to the ProducerConsumerClient in the Nix store. Try:

> ls -l result/bin/producer-consumer-client.jsexe/

Where you should see some files including javascript and html files.

Next the server part. The server parts needs access to the client. We can achieve this by creating a Nix expression pointing to both client and server. Create packages.nix:

{ pkgs ? import <nixpkgs> {} }: rec { client = import ./client { }; server = import ./server { inherit client; }; }

The server will be a simple Snap application, which just serves the JavaScript files created by ProducerConsumerClient.

We need a server directory:

> mkdir server > mkdir server/src

And server/default.nix:

{ pkgs ? (import <nixpkgs> {}) , hp ? pkgs.haskellPackages_ghc784 , client }: hp.cabal.mkDerivation (self: { pname = "ProducerConsumerServer"; version = "1.0.0"; src = ./.; enableSplitObjs = false; buildTools = [ hp.cabalInstall ]; isExecutable = true; isLibrary = false; buildDepends = [ hp.MonadCatchIOTransformers hp.mtl hp.snapCore hp.snapServer hp.split hp.systemFilepath client ]; extraLibs = [ ]; preConfigure = '' rm -rf dist ''; postInstall = '' # This is properly not completely kosher, but it works. cp -r $client/bin/producer-consumer-client.jsexe $out/javascript ''; inherit client; })

And server/ProducerConsumerServer.cabal:

Name: ProducerConsumerServer Version: 1.0 Author: Author Category: Web Build-type: Simple Cabal-version: >=1.2 Executable producer-consumer-server hs-source-dirs: src main-is: Main.hs Build-depends: base >= 4 && < 5, bytestring >= 0.9.1 && < 0.11, MonadCatchIO-transformers >= 0.2.1 && < 0.4, mtl >= 2 && < 3, snap-core >= 0.9 && < 0.10, snap-server >= 0.9 && < 0.10, split >= 0.2.2, system-filepath >= 0.4.13, filepath >= 1.3.0.2 ghc-options: -threaded -Wall -fwarn-tabs -funbox-strict-fields -O2 -fno-warn-unused-do-bind

And server/src/Main.hs:

{-# LANGUAGE OverloadedStrings #-} module Main where import Prelude hiding (head, id, div) import qualified Prelude import Snap.Core (Snap, dir, modifyResponse, addHeader) import Snap.Util.FileServe (serveDirectory) import Snap.Http.Server (quickHttpServe) import System.Environment (getEnvironment, getEnv, getExecutablePath) import System.FilePath import Data.List.Split (splitOn) import Data.List (isInfixOf) main :: IO () main = do exePath <- getExecutablePath let baseDir = takeDirectory exePath ++ "/../javascript/" quickHttpServe $ site baseDir getClientDir :: IO String getClientDir = do getEnvironment >>= mapM_ print nativeBuildInputs <- getEnv "propagatedNativeBuildInputs" return $ Prelude.head $ filter (isInfixOf "my-test-app") $ splitOn " " nativeBuildInputs site :: String -> Snap () site clientDir = do Snap.Core.dir "client" (serveDirectory clientDir) let header key value = modifyResponse (addHeader key value) header "Cache-Control" "no-cache, no-store, must-revalidate" header "Pragma" "no-cache" header "Expires" "0"

Now we can compile the client, using packages.nix, and the server:

> nix-build -I . packages.nix -A client > nix-build -I . packages.nix -A server

Now it is time to run the application:

> result/bin/producer-consumer-server

and point your browser to http://localhost:8000/client. You should see the numbers one, two, and three. After about a second you should see the numbers lists changing, as the producer and consumer changes the list.