In this post we’ll try to remove the JS-ey part of our application – the Julius templates. Before we move on to the actual implementation, I have to warn you: GHCJS is not yet a fully mature project and installing it on Windows platform is a pain. I wasted nearly two days trying to do it using stack, mostly due to a problem with locale – The Internet said it’s already solved, but for some reason it didn’t work. Only the solution described in https://github.com/commercialhaskell/stack/issues/1448 worked – setting non-UTF application locale to en_US) – still, that’s not all. I also had to install pkg-config (this StackOverflow question is about “how to do it”) and nodejs, and that’s still not all – older versions of GHCJS (for example one proposed on Stack GHCJS page) have some trouble with Cabal. In the end, I had to install GHCJS from sources on Windows. Surprisingly, it went without serious problems.

On Linux machines it also requires nasty hacks – like symbolic link from node to nodejs. During standalone installation this can be solved by --with-node nodejs flag, but not on stack installation (unless it’s somehow configurable in stack.yaml, which I’m not aware of).
Installation takes really long – I suppose it was over an hour and downloaded like, half of The Internet.

Anyway, since I’ve managed to do it, you should be able to install it too (if you’re not – write in comments, maybe I can help?), so let’s go to teh codez!
First let’s create the GHCJS project, as a subproject to our main one. Go to main project directory and run stack new ghcjs. That created some files, but – surprisingly – not related to GHCJS in any way. To request compiling it with GHCJS, you have to add the following code to your stack.yaml:

resolver: lts-6.18
compiler: ghcjs-0.2.0.9006015_ghc-7.10.3
compiler-check: match-exact
setup-info:
  ghcjs:
    source:
      ghcjs-0.2.0.9006015_ghc-7.10.3:
        url: "https://tolysz.org/ghcjs/lts-6.15-9006015.tar.gz"
        sha1: 4d513006622bf428a3c983ca927837e3d14ab687

If you are wondering where did I get these paths from (and you should be – never trust an unknown blogger asking you to install some arbitrary packages!), it’s from a ghcjs-dom GitHub issue.

After that run stack setup and spend some time solving the problems (it takes some time, not counting really long installation). For example, if you’re using Windows, you won’t be able to set up environment, because this package uses a particular resolver – lts-6.15, which has a built-in version 0.8.18.4 of yaml package, which cannot be built on Windows, because it contains non-ASCII characters in the path (its this problem). Seriously, that’s one of weirder problems I’ve encountered. Bad news is that it’s not possible to elegantly solve it. I changed manually the downloaded package to use lts-6.18 resolver, which works fine. If you choose this solution, remember to remove sha1 from stack.yaml. Also remember that creating tar.gz files on Windows work a bit differently than on Linux and you may have trouble during repacking (luckily, 7-Zip offers an in-place change feature, which solves this issue).

And voila – we’re ready to code!
First, let’s check whether the current version (which – by default – does only some printing to the console) works with Yesod.
To do that, first compile the GHCJS project (simply stack build), then copy all.js file from .stack-work/install/[some-hash]/bin/*.jsexe to a new directory, static/js. Then we need to includ this file – this is a little cumbersome in Yesod, but adds to type-safety. In desired handler (in our case Project.hs) you have to add addScript $ StaticR js_all_js to implementation of renderPage. The way of inclusion is a bit weird (substituting all slashes and dots with underscores), but guarantees compile-time safety (if the file does not exist, the app will not compile), which is good.

After these changes, run stack exec -- yesod devel. Here’s a fun fact: most probably it won’t compile, saying that js_all_js is not in scope. Thanks to this StackOverflow question we can detect the problem – Yesod didn’t recognize a new static file (identifiers are automatically generated for them). You need to either change or touch Settings/StaticFiles.hs, and after recompilation it’ll work.

Now run the server and open a project. And yay, it works! You can verify it by someFunc print in the JavaScript console. So far so good, now we have to find a way to perform the same action we did in project.julius. There are three steps to perform:

  1. Hook on #delete-project button
  2. onClick – issue a DELETE request
  3. after a response is received – redirect user to site of response

In JavaScript it was really simple, how will it look like in Haskell? Let’s find out!
First thing we need to do is adding ghcjs-dom to our dependencies: extra-deps: [ghcjs-dom-0.3.1.0, ghcjs-dom-jsffi-0.3.1.0] to stack.yaml (FFI is also required) and to .cabal file. Turns out we also have to add ghcjs-base-0.2.0.0 to stack.yaml, but apparently it’s not a published package… luckily, stack can address this. To packages field in stack.yaml add:

- location:
   git: https://github.com/ghcjs/ghcjs-base.git
   commit: 552da6d30fd25857a2ee8eb995d01fd4ae606d22

this will fetch the latest (as of today) commit from ghc-js. This is version 0.2.0.0, so it’ll compile now, right? Right. But wait we didn’t code anything yet! Oh dear, let’s fix it now.

Open the ghcjs/src/Lib.hs file and write:

module Lib (setupClickHandlers) where

import GHCJS.DOM
import GHCJS.DOM (runWebGUI, webViewGetDomDocument)
import GHCJS.DOM.HTMLButtonElement (castToHTMLButtonElement)
import GHCJS.DOM.Document (getElementById)
import GHCJS.DOM.EventTarget
import GHCJS.DOM.EventTargetClosures
import GHCJS.DOM.Types

setupClickHandlers :: IO()
setupClickHandlers = do 
  runWebGUI $ \webView -> do
    Just doc <- webViewGetDomDocument webView
    Just element <- getElementById doc "delete-project"
    button <- castToHTMLButtonElement element
    clickCallback <- eventListenerNew printNow
    addEventListener button "click" (Just clickCallback) False

printNow :: MouseEvent -> IO()
printNow _ = print "Clicked!"

We won’t get through all the imports (I was heavily inspired by some GitHub examples) – it is possible that some of them aren’t necessary, and others can definitely be scoped. Nevertheless, I think that going through the code will be hard enough, so let’s ignore the details for now.

First call, runWebGUI, is responsible for setting our code in proper context. It calls given function (a lambda, in our case) with the GUI view. It’s pretty cool that you can use exactly same code for both the browser and native GTK apps (with proper linkage, obviously). Then we extract the document DOM from the GUI and desired button from the document. In the next line, we create a callback (from function defined a few lines lower), and attach it to "click" event of our button. The syntax for the listener might seem a bit weird, so let’s take a look at the signature and definition:

addEventListener ::
                 (MonadIO m, IsEventTarget self, ToJSString type') =>
                   self -> type' -> Maybe EventListener -> Bool -> m ()
addEventListener self type' listener useCapture

The first two arguments – event target (button) and event type (click) are fairly intuitive, but why is EventListener a Maybe, and what is useCapture? useCapture is a parameter controlling the way of event propagation. It’s explained in more detail here (link from ghcjs-jsffi source). Unfortunately, I still do not know why is EventListener a Maybe – possibly to allow change of event propagation without any actual handler? If you have an idea, let me know in the comments!

You also need to call this function (instead of someFunc) in app/Main.hs. Then compile, copy all.js to static/js (just like before) and remove templates/project.julius file. Now, be careful, this might hurt a little: yesod devel alone won’t spot that you’ve removed the file, so you’ll get 500: Internal Server Error when project.julius should be used.

The current version implements 1st point from our checklist – we’ve added a hook on #delete-project button. Now is the time for some bad news – we won’t be able to easily use Yesod’s typesafe routes. Obviously, we can work around it by generating an interface file – but that’s another level of infrastructure we need to build. That’s why we’ll leave typesafe routes for now and stick with typical strings.

With that knowledge, let’s implement the AJAX call:

data DeletionResponse = DeletionResponse { target :: String } deriving (Generic, Show)
instance ToJSON DeletionResponse where
  toEncoding = genericToEncoding defaultOptions
instance FromJSON DeletionResponse

makeRequest :: String -> Request
makeRequest projectId = Request {
  reqMethod = DELETE,
  reqURI = pack $ "/project/" ++ projectId,
  reqLogin = Nothing,
  reqHeaders = [],
  reqWithCredentials = False,
  reqData = NoData
}

requestProjectDeletion :: String -> IO (Response String)
requestProjectDeletion projectId = xhrString $ makeRequest projectId

deleteProject :: MouseEvent -> IO()
deleteProject _ = do 
  currentLocation <- getWindowLocation
  currentHref <- getHref currentLocation
  response <- requestProjectDeletion $ unpack $ extractLastPiece currentHref
  redirect currentLocation currentHref response
  where
    redirect currentLocation oldHref resp = setHref (pack $ getTarget oldHref resp) currentLocation
    extractLastPiece href = last $ splitOn' (pack "/") href
    getTarget fallback resp = maybe (unpack fallback) target $ ((BS.pack `fmap` contents resp) >>= decode

I’ve also added a few imports (Data.JSString, JavaScript.Web.XMLHttpRequest, JavaScript.Web.Location, GHC.Generics, Data.Aeson and Data.ByteString.Lazy.Char8 as BS) and a language extension – DeriveGeneric, for automatic generation of Aeson serialization/deserialization routines. Of course, this required changes in cabal file as well (aeson and bytestring dependencies). deleteProject becomes our new clickCallback and it works the same way as earlier again!

Now, that’s quite a lot of code, so let’s go through it and examine what happens there.
We start with definition for our response data type – to be honest, it’s not really necessary, and we could’ve just extracted the target field from the response JSON, without intermediate Haskell objects. If GHCJS offers some helpers to do that (with Aeson alone it wouldn’t be much simpler), it could spare us a few packs and unpacks.
Next we create a HTTP request – only variable part here is URI, determined by projectId. An important thing to note is that this code works slightly different thatn the previous version (in Julius) – we only have one code and it dynamically determines routes – previously a separate code was generated and sent with each page, which could add to delays (if it got bigger). Files generated by GHCJS are fairly big (hundreds of kBs), so we can’t really afford sending dozens of them to each user – network bandwidth might be cheaper than earlier, but it won’t be cheap enough to simply throw the throughput away.
Fun fact: the first time I’ve implemented it in a way that it compiled, I mistyped the route. Lack of static typing for routes is quite sad, but probably solvable with some work.
And then, after a short AJAX wrapper, we have the main event listener – deleteProject. It starts with determining the current path, for two reasons – first, that’s the location to set if something goes wrong (“no change”), and second – to determine ID of project. While it works now, it poses several threats. First of all, if two teams work separately on frontend and backend, at some point the route will change (probably without notice) and this mechanism will break. This can be of course prevented with thorough testing and strict processes, but there is also second problem – no URL minifiers will work. While this might not be a problem, it may become one when you switch to MongoDB identifiers.
Next line might be one of the most interesting features of Haskell in asynchronous applications. Due to lazy I/O, we can request redirection to be performed “when data is ready” (after response is received – response is required to proceed here). That’s a really nice solution compared to chains of promises (which are also really nice compared to typical Node.js callbacks), which doesn’t break the code flow but – at the same time – performs implicit waits wherever needed.

The time has come for some final thoughts – after all, we managed to implement the same (or at least very similar), simple functionality using GHCJS and Julius. From my perspective, using GHCJS for simple scripts is a vast overkill – JavaScript is sufficient for it, and if you want more type safety, choose TypeScript (it is also supported in the Shakespearean family). You get out-of-the-box integration with Yesod, a simple type system and route interpolation. That might not be much, but remember that right now we’re aiming for rather simple scripts. And hey – people write *big* apps in JavaScript with no types, so a few lines are not a tragedy (if required).
As for GHCJS – it’s a powerful and promising tool, but still very immature. It’s targets are ambitious, but for now it simply isn’t usable (at least on Windows) – that’s simply unacceptable for an installation from packages to take over two days and require to look through dozens of GitHub issues. Installation from sources might be more convenient, but I expect a mature tool to provide an installable package (even if all the package does is instrumenting the entire compilation locally). And more importantly – if it provides any package, it should work out-of-the-box (regardless if it’s old or new – assuming it’s the newest one, older may have bugs). Right now start-up overhead is simply too big to be acceptable, at least for me (over a half of this post is just about setting up GHCJS!). Programming in it is quite nice, but documentation is ultra-sparse, and most of the stuff has to be looked up in the source – that’s also not what I’d expect from a mature tool. Nevertheless, GHCJS caught my attention and I’ll definitely take a closer look at it again in several months. Maybe then it’ll be possible to apply it to some bigger project (for small ones infrastructure costs – setups, installations etc. – are much too high for me).

Looks like I’ll have to look for a different tool for frontend development (assuming I’m not happy with interpolated JavaScript/TypeScript/CoffeeScript, which is true). I’m going to consider Elm as the next tool – while it’s not exactly Haskell, for a first glance it looks quite haskelly, has static types and several other nice features, as well as decent performance and some Yesod integration. Perhaps it’s worth checking in one of the next posts?

Stay tuned!