-
Notifications
You must be signed in to change notification settings - Fork 6
Tutorial
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:
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.
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!
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;
alert(c3);
})();
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");
v1.fillRect(10,10,100,100);
})();
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
.
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.
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 oversyncJS
andasyncJS
. 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.
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
setTimeout
).
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 JSMVar
s 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!