Skip to content

Latest commit

 

History

History
192 lines (154 loc) · 6.74 KB

2013-12-23-24-days-of-hackage-fay.md

File metadata and controls

192 lines (154 loc) · 6.74 KB
title
24 Days of Hackage: fay

A few days ago, we looked at the websockets library. The server side was great - lots of Haskell! However, writing the client left a bit to be desired - we had to write JavaScript and HTML, yuck! Last year we saw that blaze-html gives us a nice EDSL for HTML generation, but what about JavaScript? Today we're going to look at a slightly different approach, and look at Fay.

Rather than providing a library for GHC Haskell, Fay is a Haskell-to-JavaScript compiler - specifically allowing us to compile a subset of Haskell to JavaScript. Of these differences, the biggest is the lack of type classes, though many experienced Fay developers tend to feel this can be worked around. Let's jump right in and rewrite our websockets client using Fay. First, a review of the existing JavaScript code:

var prompt = document.getElementById('prompt');
var connectionToSanta = new WebSocket("ws://localhost:8080/");
var messages = document.getElementById('repl');

function log(message, cls) {
    var messageNode = document.createElement("li");
    messageNode.innerHTML = message;
    messageNode.className = cls;
    repl.appendChild(messageNode);
}

connectionToSanta.onopen = function(e) {
    prompt.disabled = false;
    prompt.onkeydown = function (e) {
        if (e.keyCode == 13) {
            connectionToSanta.send(prompt.value);
            log(prompt.value, "me");
            prompt.value = "";
        }
    }
};
connectionToSanta.onmessage = function (m) {
    log(m.data, "");
}

There's not a lot of JavaScript code here, but you'll see that we do exercise a reasonable amount of the language. We're using a few variables, some functions for callbacks, defining common functionality via a named function, and making API calls to create WebSockets.

First, lets look at the log function - that looks easy enough. We need to create a new DOM element, set a few properties on it, and then append it to another document element. Fay doesn't come with functions to do this directly, but it does expose a foreign function interface that allows us to easily translate Haskell code into JavaScript calls. To do this, we simply create a function who's body is a call to ffi, and provide an associated type signature. For createElement, we have:

data Element

createElement :: String -> Fay Element
createElement = ffi "document.createElement(%1)"

The type signature indicates the type of the foreign function, and we use ffi to describe what actual code to run. Fay will interpolate function arguments into the JavaScript, using placeholders marked by %1, etc. We operate in the Fay monad, for the same reason we do potentially real-world side effecting work in the IO monad.

We've also introduced a Element type, which uses the EmptyDataDecls extension to add no constructors. This means the only way to create Elements is by calling out via foreign functions.

We need a more ffi routines to set the innerHTML and className, and another to append one Element to another. We write these in much the same way:

setInnerHTML :: Element -> String -> Fay ()
setInnerHTML = ffi "%1['innerHTML'] = %2"

setClassName :: Element -> String -> Fay ()
setClassName = ffi "%1['innerHTMl'] = %2"

appendChild :: Element -> Element -> Fay ()
appendChild = ffi "%1.appendChild(%2)"

Now we can write our log function:

log :: Element -> String -> String -> Fay ()
log container msg cls = do
  logNode <- createElement "li"
  setInnerHTML logNode msg
  setClassName logNode cls
  container `appendChild` logNode

Here we simply use the Fay monad to sequence DOM-manipulating operations. As before, we create a new li element, set the class and content, and then append this to a log container. Because in Haskell we don't have global variables, we have to explicitly pass in the log container - which is arguably more maintainable code anyway!

Now that we've seen how the FFI works, we can also see how to write the first lines of our script - we need FFI routines to select the prompt and log container elements, and one more call to create our WebSocket:

getElementById :: String -> Fay Element
getElementById = ffi "document.getElementById(%1)"

data WebSocket

newWebSocket :: String -> Fay WebSocket
newWebSocket = ffi "new WebSocket(%1)"

To create new objects the syntax is the same as creating objects in JavaScript, and the same ffi call in Haskell - which is nice for consistency. We introduce a new type for WebSockets, in order to leverage the lovely benefits of type-safety in our Fay code.

Finally, we need to deal with callbacks. This is actually really simple and the answer is - you guessed it - another ffi declaration. This time, we'll be a little more general, and create a ffi binding to add an event listener in the general case:

class Eventable

data Event

instance Eventable WebSocket
instance Eventable Element

addEventListener :: Eventable a => a -> String -> (Event -> Fay ()) -> Fay ()
addEventListener = ffi "%1[%2] = %3"

Here we use the limited type class support in Fay to create a class of Eventable objects - and both Elements and WebSockets can have event listeners.

There are only a few more ffi bindings to go now, and then we're ready to write the bulk of our application. I'll leave the bindings to the associated code, and lets look at how our application looks when we put it all together:

main :: Fay ()
main = do
  prompt <- getElementById "prompt"
  messages <- getElementById "repl"
  connectionToSanta <- newWebSocket "ws://localhost:8080"

  addEventListener connectionToSanta "onopen" $ \_ -> do
    setDisabled prompt False
    addEventListener prompt "onkeydown" $ \e -> do
      if (eventKeyCode e == 13)
        then do
          let message = elementValue prompt
          connectionToSanta `send` message
          log messages message "me"
          clearValue prompt
        else return ()

  addEventListener connectionToSanta "onmessage" $ \e -> do
    log messages (messageData e) ""

To use this from the HTML page, we just need to drop in:

<script src="2013-12-23-fay.js" type="application/javascript"></script>

Once we include this, our page behaves exactly as before. Except we also get that warm fuzzy feeling of knowing that we got to write Haskell to generate all of this.

The code for today's post can be found on my Github account, as always. Make sure to join me again tomorrow of for this year's final 24 Days of Hackage post as we'll take a look back at the posts this months - with a twist.