Wednesday, October 20, 2010

Recompile your Haskell-based templates faster than you can hit F5.

There are two main classes of templating solutions in Happstack:

1. DSLs/libraries such as BlazeHtml, HSP, Hamlet, etc, where your templates are written in Haskell and compiled at compile time.

2. Libraries like heist, HStringTemplate, etc, where your templates are written in some external template file and read at runtime by the server.

Each method has strengths and weaknesses -- and so each project needs to pick the solution that works best for them.

For my projects I love using HSP. I like having the full expressive power of Haskell in my templates, and the added safety that the type checker provides. But I hate having to recompile, relink, and restart my app server dozens and dozens of times when I am developing my templates. And, so it is with great pleasure that I present the triumphant return of happstack-plugins!

happstack-plugins


happstack-plugins leverages the recently revived plugins package so that individual page templates can be automatically recompiled and reloaded into a running happstack application. happstack-plugins uses hinotify to watch the haskell source files containing your page templates. Whenever you save changes, the page is automatically recompiled and reloaded into the running server. Typically this happens fast enough that by the time you switch to the browser and hit reload, the updated page is already available.

You can see a demo of happstack-plugins in action here:



How to use happstack-plugins



Using happstack-plugins is very straight-forward. First you need to install the happstack-plugins library which is currently only available in the happstack darcs repository:


darcs get http://patch-tag.com/r/mae/happstack


For best performance you should put each page template in its own module so that it can be recompiled and reloaded faster.

The templates themselves require no special modifications. Here is a simple helloPage template:

> module HelloPage where
>
> import Happstack.Server
>
> helloPage :: String -> ServerPart Response
> helloPage noun = ok (toResponse $ "hello, " ++ noun)

This template takes a single String argument and returns a text/plain page which says, "hello, <string>". We could just as well use BlazeHTML, HSP, etc, but using String keeps this example short and simple.

As I mentioned, there is nothing new going on here, it just a normal happstack ServerPart.

The interesting changes are in the Main module. There are only 3 simple changes required to support templates. But first, some boring stuff at the top of the module:



> {-# LANGUAGE CPP, TemplateHaskell #-}
> module Main where
>
> import Control.Monad (msum)
> import Happstack.Server



1. Here we #ifdef some module imports. These two modules provide the same interface. The Dynamic version actually does page recompilation and reloading. The Static version just links things in the normal way. This makes it easy to use dynamic loading during development but static linking for the live server by simply defining or undefining PLUGINS.



> #ifdef PLUGINS
> import Happstack.Server.Plugins.Dynamic
> #else
> import Happstack.Server.Plugins.Static
> #endif
> import HelloPage






2. In main we call initPlugins which starts the recompiler/reloader and hinotify. If you import Happstack.Server.Plugins.Static, initPlugins is a 'noop', so we do not have to add any extra #ifdefs.



> main :: IO ()
> main =
> do ph <- initPlugins
> simpleHTTP nullConf $ pages ph



3. Here is where we actually specify a template to load dynamically:



> pages :: PluginHandle -> ServerPart Response
> pages ph =
> msum [ $(withServerPart 'helloPage) ph $ \helloPage ->
> (helloPage "hello")
> ]



Normally we would just have:


> pages :: PluginHandle -> ServerPart Response
> pages ph =
> msum [ helloPage "world"
> ]


So the new part is the template haskell function withServerPart which effectively takes three arguments:

1. the name of the symbol to dynamically load
2. the PluginHandle which initPlugins returned
3. a function which will use the loaded symbol

so, withServerPart effectively has the type:



> withServerPart :: (MonadIO m, ServerMonad m) => Name -> PluginHandle -> (a -> m b) -> m b



Even though we are dynamically reloaded the page at runtime, the compiler will still check that the types are correct when will compile the main application.

If we change helloPage "hello" to helloPage 1 and try to build Main.lhs we will get the error.



Main.lhs:50:28:
No instance for (Num String)
arising from the literal `1' at Main.lhs:50:28
Possible fix: add an instance declaration for (Num String)
In the first argument of `helloPage', namely `1'
In the expression: (helloPage 1)
In the second argument of `($)', namely
`\ helloPage -> (helloPage 1)'
Failed, modules loaded: HelloPage.



What's left to do?



There are two big features on the TODO list. If you think happstack-plugins is cool, I encourage you to work on them!

1. The underlying plugins library is broken when it comes to hierarchical modules. Ideally I would put all the pages in Pages.*. For example Pages.HelloPage. But, that does not work. As a hack, you can modify System.Plugins.Make.build and comment out output in the let flags = ... declaration. This fixes hierachical modules, but requires you to run your app with its working directory set to the root directory of your project. That is fine for happstack app development, but not an ideal solution for all users of the plugins library. If someone could fix hierarchical module support in plugins, that would be great for everyone.

2. hinotify is only supported under Linux. However, it should not be that hard to make hinotify support optional (via a compile time flag). With out hinotify, we would just do a quick stat() everytime the template is invoked and see if a recompilation is needed. When a compilation is needed, you will have to wait for that page to recompile and reload -- but it will still be much faster than rebuilding and restarting the whole server.

3 comments:

  1. That's Fantastic!

    I hate long restart times, and this fixes that problem.

    ReplyDelete
  2. @Edward September,

    http://www.haskell.org/pipermail/haskell-cafe/2010-September/083965.html

    ReplyDelete