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.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: