Let's Play!

Inspired by a talk by John Carmack [follow-up post], I wrote these browser games in Haskell. I used the Haste compiler to solve the JavaScript problem.

I chose Haste because GHCJS seemed tough to install at the time.

I could mostly pretend it was Haskell as usual, though some differences arose.

Sadly, the Haste compiler appears to be abandoned now. On the other hand, it looks like GHC is gaining JavaScript and WebAssembly backends. Also, I’ve been experimenting with my own Haskell compiler, which is good enough for simple web games.

JavaScript FFI

Calling Haskell functions from JavaScript and vice versa is painless. We’ll demonstrate calling Haskell from JavaScript and vice versa:

Let’s walk through the details.

Strings are a constant source of friction, as JavaScript and Haskell have represent them differently. (In fact, Haskell has many representations, and its most well-known one, String, is often terrible.)

Enabling the OverloadedStrings extension marginally reduces the number of conversions between String and JSString.

{-# LANGUAGE OverloadedStrings #-}
import Data.List
import Data.Maybe
import Haste.Foreign
import Haste.Prim

Our demo features a mind-blowing Haskell one-liner, the function readMany, which extracts readable Haskell values of a given type. The wow functions calls it to extract values of type Int, Double and String from a given JSString, and shows the results in a JSString.

We call ffi to grant our code access to the JavaScript termOpen function, which opens the terminal. The type declaration here is mandatory.

The export function performs the converse, that is, grants JavaScript access to our wow function.

readMany :: Read a => String -> [a]
readMany = unfoldr $ listToMaybe . concatMap reads . tails

wow :: JSString -> JSString
wow js = let s = fromJSStr js in toJSStr $ unlines $ [
  "Ints:    " ++ show (readMany s :: [Int]),
  "Doubles: " ++ show (readMany s :: [Double]),
  "Strings: " ++ show (readMany s :: [String])]

main :: IO ()
main = do
  export "wow" wow
  ffi "termOpen" :: IO ()

The above ties in with the JavaScript below. The termlib.js refers to the excellent termlib JavaScript library.

<style type="text/css">
.term {
  font-family: 'Inconsolata', monospace;
  font-size: 90%;
  color: #00aa00;
  background: #000000;
}
.term .termReverse {
  background: #00aa00;
  color: #000000;
}
table { margin: 0; }
</style>
<script type="text/javascript" src="/~blynn/termlib.js"></script>
<script type="text/javascript">
var term = new Terminal( {handler: termHandler, greeting:
  'Try typing: foo "quotes!!11!" 42 3.1415 and 9e3',
  cols: 48, rows: 16} );
function termHandler() {
  this.newLine();
  var line = Haste.wow(this.lineBuffer);
  if (line != "") this.write(line);
  this.prompt();
}
function termOpen() { term.open(); }
</script>
<div id="termDiv"></div>
<script type="text/javascript" src="index.js"></script>

Threads

JavaScript is single-threaded, in the sense that a function is run to completion before another begins. There is a implicit event loop running the show.

Redraws only occur when control is returned to the event loop, which can require sending timeout events.

When programming with libraries such as SDL, we manage multiple threads, and redraws are under our control. This might make it challenging for a browser game and a Linux game to share code, though I’ve only explored this briefly. Perhaps writing an event loop to mimic JavaScript is the easiest solution.

An alternative may be Haste.Concurrent, which appears to simulate multiple threads in JavaScript.

HTML Canvas

Experience with SDL helps somewhat with drawing to a canvas element. For example, createRGBSurface is analagous to createCanvas.

On the other hand, instead of pixels with integer coordinates, the canvas uses points with floating point coordinates.

UI woes

Haste has limited support for HTML events. For an Enigma machine simulation, I wanted to intercept the input event. The Haste API lacks this event.

I worked around the problem by writing a snippet of JavaScript to fire off a scroll event on an input event, which Haste does recognize:

<script type="text/javascript">
var ev = new Event('scroll');
function genEv() {
  document.getElementById("grundstellung").dispatchEvent(ev);
}
</script>
<textarea oninput="genEv();"></textarea>

Big data

I gave up trying to embed a neural network and some test cases in a handwritten digit recognition demo.

I tried defining a giant list in Haskell. I tried a routine to fetch and read a text file. In the end I was forced to write some JavaScript.

haste-cabal

For a long time, I thought I was confined to the packages bundled with Haste.

On the contrary, by running haste-cabal, we can use many packages in our JavaScript programs:

$ haste-cabal update
$ haste-cabal install random
$ haste-cabal install parsec

Ben Lynn blynn@cs.stanford.edu 💡