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.