RyanGlScott edited this page Jan 2, 2015 · 24 revisions

This is supposed to be a quick and dirty tutorial to give you a feeling for what can be done with Sunroof and where to look to get started. If you need more detailed information look at the sources and generate the documentation from them.

If you want to dive into code right away the Sunroof examples are also a good starting point to see how Sunroof can be used.

Here an overview of the tutorial:

Part 1: Introduction to Sunroof

To install Sunroof you have to check out a few packages from GitHub. Please, look into the provided readme to find further instruction. It will be up to date.

Lets look at expressions in Sunroof first and then move on to monadic statements.

Expressions: Basic building blocks

All types in Sunroof that represent a JavaScript value and therefore can form expressions in JavaScript implement the class Sunroof. Everything in this section works when importing the following packages:

import Data.Default     -- Provides defaults for value
import Data.Monoid      -- Monoids are important for strings
import Data.Boolean     -- Overloaded booleans
import Language.Sunroof -- All Sunroof functionality

If you want to play around with the compiler you can use this function to compile your expressions directly:

test :: (Sunroof a) => a -> IO ()
test o = sunroofCompileJSA def "main" (return o) >>= putStrLn

Lets look at the basic types that are provided. First of all there is unit. It can be thought of as an equivalent of void or null in JavaScript, because it indicates nothing of interest.

Of course, there is a boolean type JSBool. Sunroof uses the Data.Boolean package to overload booleans. There are constants true and false as well as the usual operators &&*, ||* or notB. Branches can be expressed using the ifB function. If you need a standard operator involving booleans usually you can just append a star (*) for the overloaded version. The overloaded version of a function can be found by appending B to its name.

ifB (true &&* false) (notB false) (false ||* true) :: JSBool

The type JSNumber represents JavaScript numbers. They can be thought of as Double values. Thanks to the generality of the Num class numeric expressions can be written as usual.

(3 + 4) * 5 :: JSNumber

Following the naming scheme mentioned above there are also overloadings of the operators ==, /=, < and > (provided by the classes EqB and OrdB). Just append a star to use them.

Lets look at an example for all this in Haskell:

(x - 5 >= -10 && x * 2 <= 10) && (x /= 0 || not (y + 3 == x)) :: Bool

How would this look as a JavaScript expression in Sunroof?

(x - 5 >=* -10 &&* x * 2 <=* 10) &&* (x /=* 0 ||* notB (y + 3 ==* x)) :: JSBool

What other types are there? JSString represents strings. We did not overload operators like ++, but JSString is a instance of Monoid, therefore you can just use <> instead.

"Your name is " <> name

In case you are wondering how we can use a Haskell string literal here: To do that you need to activate the GHC language extension OverloadedStrings. In case you do not want to do that, you can just use the string conbinator to convert a Haskell string into a Sunroof string.

There also is JSArray a which can roughly be thought of an equivalent to [a]. You can create your own array instances from lists using the array combinator.

array [0 .. 5 :: Int]

This seems nice and type safe, does it not? But JavaScript does not hava static typing you might say. Of course, you are right. In case you really need to convert types into each other Sunroof provides the cast function which can convert any Sunroof type into another one.

cast :: (Sunroof a, Sunroof b) => a -> b

You will also encounter JSObject. There is no direct equivalent to this type in Haskell. The closest you can get is Sunroof a => Map String a. JSObject is important, because it represents everything that Sunroof cannot represent. When using the JavaScript APIs provided by Sunroof you will often encounter JSObject.

To access the attributes of an object or entries of an array you can use the ! operator together with the combinators index or label.

arr ! index 0
obj ! label "name"

Now lets move on to statements!

Statements: Sequential code

Sunroof provides a deep embedding of JavaScript into Haskell. All code written is structured in a monad to capture its sequential nature. Also a major difference between monadic statements and expression, like we have seen in the previous section, is that expressions provided by Sunroof can be assumed to be free of side effects. Everything inside a monadic statement may have a side effect in JavaScript. In fact binding itself is the most basic side effect, an assignment to a variable.

The central monadic type in Sunroof is JS t a. The t type parameter represents the threading model. We will talk about it later and ignore it for now. For what we want to look into here, you can just use the short-hand JSA a instead of JS t a.

To compile the following examples you can use the following function:

testJS :: JSA () -> IO ()
testJS o = sunroofCompileJSA def "main" o >>= putStrLn

To get started with the JS monad lets look at a small example:

askName :: JSA ()
askName = do
      name <- prompt "What is your name?" ""
      alert $ "Your name is " <> cast name <> "!"

This look pretty close to what you would write in JavaScript, right? It actually translates into something pretty similar to what you would expect:

var main = (function() {
  var v0 = prompt("What is your name?","");
  var c2 = v0+"!";
  var c3 = "Your name is "+c2;

All statements are wrapped into the local scope of a function. This protects the global JavaScript namespace from being polluted with all the new variables produced by Sunroof. Also it prevents us from getting in conflict with global bindings. At the same time effects can escape the scope. If the compiled Sunroof has a interesting result it is returned by the function. Looking back at the test function you can see how this works.

The monadic binding (<-) can be thought of as assignment to a new variable (it is literally translated to that). Statements that produce unit as return value are not assigned to a variable, because assigning void to a variable is not a useful thing to do.

The prompt and alert are top level JavaScript functions provided by the Language.Sunroof.JS.Browser module for convenience.

Lets look at another example. This time we also want to call methods from a object, instead of just calling top level functions:

drawBox :: JSA ()
drawBox = do
      canvas <- document # getElementById "canvas"
      context <- canvas # getContext "2d"
      context # fillRect (10, 10) (100, 100)

It results in the following code:

var main = (function() {
  var v0 = document.getElementById("canvas");
  var v1 = v0.getContext("2d");

The #-operator is analog to the .-operator, because we do not want to get in conflict with function composition.

The document object is a JSObject. getElementById returns the DOM object of an element with the given id. Assuming that element is a canvas we can call getContext on it and use the context to draw a square with the fillRect method.

document and getElementById are also provided by the Language.Sunroof.JS.Browser module. getContext and fillRect are part of the HTML5 canvas API which is provided through Language.Sunroof.JS.Canvas.

The Compiler

To compile your JS monad you can use the sunroofCompileJSA function.

compileSunroofJSA def "main" example

The first parameter contains the compiler options. Just use the options provided by def (from Data.Default) to get started. The second argument is the name of the variable the result of your code is assigned to. The third and last parameter is the JS monad you want to compile.

This is all you need to know to get started writing your own JavaScript with Sunroof. The next two chapter will explain the threading models and how you can write server based applications using the sunroof-server package.

The examples provided here are collected in this file.

Part 2: Server Applications

If you are interested in writing more complex application that require communication between client and server you should look into the sunroof-server package. It provides ready to use infrastructure for setting up a Kansas Comet server and communicating with the browser. This makes it possible to interleave Haskell and JavaScript computations as needed.

Lets look at an example of its usage.

main :: IO ()
main = sunroofServer def $ \eng -> do
         name <- syncJS eng $ do
                   jsName <- prompt "Your name, please?" ""
                   return (cast jsName :: JSString)
         let s = "Your name is: " ++ name
         asyncJS eng $ alert (string s) 

What does this code do? First of all it starts a Sunroof webserver using the function sunroofServer:

sunroofServer :: SunroofServerOptions -> SunroofApp -> IO ()

As mentioned before we are using the default options (def) for the SunroofServerOptions. By default it will be available under localhost:3000. It will load the page index.html and will forward all files in the folders css/, js/ and img/. It requires a copy of jQuery (local copy) and jQuery JSON (local copy) to work. SunroofApp is just a synonym for SunroofEngine -> IO (). So this is your actual application code that will be run by the server when a page is requested. The SunroofEngine parameter provides information about the connection that is needed in most of the server functions.

Lets look at the first three lines and figure what they are doing:

name <- syncJS eng $ do
          jsName <- prompt "Your name, please?" ""
          return (cast jsName :: JSString)

syncJS is a server command to synchronously execute a chunk of JavaScript in the browser we are currently communicating with. The result of that JavaScript is then transferred back to the server and mapped to a Haskell version of that value.

syncJS :: (SunroofResult a) => SunroofEngine -> JS t a -> IO (ResultOf a)

So what we are actually doing here is displaying a prompt to the visitor of the website and sending the entered string back to the server for further processing. The cast is necessary, because prompt returns a JSObject, it might return null. But we decide to trust the user in this case.

Notice that syncJS blocks the server process until the execution is finished.

Of course, this process only works for a limited number of JavaScript result. See the instances of the SunroofResult type class for further information.

Now that we have the name as a actual String value in Haskell we can process it as we like. In this case we just append it to a nice greeting message.

let s = "Your name is: " ++ name

Note that we are using the actual ++-operator only defined for Haskell lists instead of the abstract monoid <>-operator. At last we want to greet the user with his name.

asyncJS eng $ alert (string s) 

This time we use asyncJS. It just executes the given JavaScript on the website and returns immediatly after it was sent off. This is perfect when we don't care about the result of our JavaScript.

asyncJS :: SunroofEngine -> JS t () -> IO ()

From this point you can just play around with the server and create some awesome Haskell-JavaScript-hybrid applications. For further information on the server and its functionality look at the documentation on Hackage or read the sources directly.

Here some hints what you might want to look at in the server package:

  • rsyncJS - Is a useful function if you want to precompile JavaScript in the browser and just need a reference to its result. It is especially useful in when handling functions.
  • Downlink & Uplink - Provide a utility to send arbitrary data to the browser or receive data from the browser. They are one way channels for communication and allow an abstraction over syncJS and asyncJS. Both of them can block when reading from them, which also makes them valuable as a synchronization mechanism.

The sources for this small example can be found in this file.

Part 3: Threading Models

As mentioned before Sunroof supports two threading models. The first type parameter of JS indicates which threading model is used. A stands for the atomic and B stands for the blocking model.

In the atomic model threading is handeled as in JavaScript. There is only one thread and callbacks are executed as soon as that thread finishes its computation or an event occurs. It is called atomic, because the computation can not be interrupted or blocked. You can use the short-hand JSA to indicate the type of atomic computations. In the previous two parts of the tutorial you also saw how to compile JSA code.

In this part we look at the blocking model. It enables you to use Haskell concurrency patterns in JavaScript. Computations in this threading model habe the type JSB.

To compile JSB code you can use the function sunroofCompileJSB. It has the same signature as sunroofCompileJSA:

sunroofCompileJSB :: CompilerOpts -> String -> JS 'B () -> IO String

We promised that the blocking model enables us to use Haskell concurrency abstractions. But how can we create multi threading in JavaScript?

First of all JavaScript does not support real multi threading. But we can emulate cooperative multi threading (For the really tough people out there: This is done through heavy use of continuations and the function

The most basic functions Sunroof offers for this purpose are forkJS and yield.

forkJS :: SunroofThread t1 => JS t1 () -> JS t2 ()
yield :: JSB ()

These functions do exactly what their counterparts in IO do. forkJS creates a new thread and yield suspends the current thread to give others a chance to run.

Well known abstractions over concurrency in Haskell are MVar and Chan. Sunroof offers the types JSMVar and JSChan as equivalents of those types in JavaScript. They behave like MVar and Chan in Haskell and suspend execution if needed.

Lets look at a simple example. It shows how we can interleave computations by using JSMVar.

interleaveJS :: JS 'B ()
interleaveJS = do
  mvarA <- newMVar "A"
  mvarB <- newEmptyMVar
  forkJS $ loop () $ \() -> do
    a <- takeMVar mvarA
    alert a
    putMVar "B" mvarB
  forkJS $ loop () $ \() -> do 
    b <- takeMVar mvarB
    goOn <- confirm b
    ifB goOn
        (putMVar "A" mvarA)
        (return ())

What is happening here? First of all we create two JSMVars to pass a token from one thread to the other. Then we fork two threads.

The loop function has the following signature:

loop :: Sunroof a => a -> (a -> JSB a) -> JSB ()

It executes the given function repeatedly, feeding its result back in as input. One important thing to note is that after each call to the given function, loop gives other threads a chance to run. As we do not have interesting state to carry on we just pass unit around.

So you can see that the first thread takes a token, alerts the user about it and then puts a token into the other JSMVar. The second thread then takes the token from the second JSMVar, also alerts the user and puts a token back into the first JSMVar. The branch is just there to give the user the possibility to stop the flood of popups at some point.

When executed you will see the messages "A" and "B" interchangeably. So you can see how each takeMVar waits until there is a token to take and each putMVar waits until there is no token inside anymore.

Sometimes you need to use JSA code within JSB. In that case you can use the function liftJS to lift the JSA into JSB. The other way around is not directly possible (though you can use forkJS to fork a new JSB thread from inside JSA.)

This should be enough to get you started with JSB. The sources for this small example can be found in this file.

Have fun with Sunroof!