Using Heist and Happstack

In the course of playing around with some of the newer Haskell web application stuff recently, I found that I really like the combination of Happstack and Heist. However, there are a few challenges in getting the two to play together well, so I thought I’d write up a description of how to do it.

Step 1: Getting the dependencies right

When creating your cabal file, you’ll generally need at least the following packages:

base (of course)

happstack-server, for the Happstack bits

heist, for the Heist bits

bytestring, since it’s used extensively with Heist

mtl, which is used in Happstack

monads-fd, which is used in Heist

Those last two make for a rather unhappy combination, and are the subject of the next step.

Step 2: Making mtl and monads-fd play nicely

This took some figuring out. Basically, a good bit of Heist uses the MonadIO class from monads-fd. At the same time, Happstack uses the MonadIO class from mtl. Left to their own devices, this will lead to a lot of errors that ServerPartT IO is not an instance of MonadIO. Of course it is… just not that MonadIO.

Here’s the code I eventually wrote to fix it. You’ll need a number of GHC extensions to do this.

{-# LANGUAGE PackageImports #-} {-# LANGUAGE FlexibleInstances #-} import "mtl" Control.Monad.Trans {- Needed because Heist uses transformers rather than the old mtl package. -} import qualified "monads-fd" Control.Monad.Trans as TRA instance TRA.MonadIO (ServerPartT IO) where liftIO = liftIO

The language extension PackageImports allows us to import modules from a specific package. In general, it’s a bad idea if you can avoid it as it can lead to fragile code… but since here we are facing the challenge of making two packages work together, there’s not another choice. We use this extension to import both the mtl and monads-fd versions of the MonadIO type class. The second language extension, FlexibleInstances, relaxes some of the Haskell98 rules regarding what kinds of instances are allowed. It is needed for the instance declaration on the last line there, and it’s pretty harmless.

Once we’ve got both versions of MonadIO and its member, liftIO, imported properly, we simply write an instance declaration making ServerPartT IO an instance of the monads-fd version of MonadIO (copying the actual behavior straight from the mtl version). Voila, problem solved.

Step 3: Porting the glue code

If you’ve worked through the Snap tutorial, you know that you can start a new Snap project by typing ‘snap init’ at the command line, and the command writes a bit of code for you. That code includes a module called “Glue” that’s largely about making Snap and Heist work together. Well, we’ll need the same code for Happstack, and have no automated command to write it for us. Not to worry, though, it’s a piece of cake to port it over, and you can tweak it as you go.

Here’s the very simple application I ended up with that uses Heist in Happstack. This is the full source code; it assumes that underneath the current working directory when the application is run, there’s a directory called “web” containing your templates, and a subdirectory of that called “web/static” containing your static files.

{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PackageImports #-} {-# LANGUAGE FlexibleInstances #-} module Main where import Control.Monad (msum, mzero) import Happstack.Server import Happstack.Server.HTTP.FileServe import Text.Templating.Heist import Text.Templating.Heist.TemplateDirectory import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy as L import "mtl" Control.Monad.Trans {- Needed because Heist uses transformers rather than the old mtl package. -} import qualified "monads-fd" Control.Monad.Trans as TRA instance TRA.MonadIO (ServerPartT IO) where liftIO = liftIO main :: IO () main = do td <- newTemplateDirectory' "web" emptyTemplateState simpleHTTP nullConf $ msum [ dir "static" $ fileServe [] "web/static", templateServe td, dir "reload" $ nullDir >> templateReloader td, ] templateReloader td = do e <- reloadTemplateDirectory td return $ toResponseBS "text/plain; charset=utf-8" $ L.fromChunks [either B.pack (const "Templates loaded successfully.") e] templateServe td = msum [ nullDir >> render td "index", withRequest (return . rqUri) >>= render td . B.pack ] render td template = do ts <- getDirectoryTS td bytes <- renderTemplate ts template flip (maybe mzero) bytes $ \x -> do return (toResponseBS "text/html; charset=utf-8" (L.fromChunks [x]))

And that’s it! You have a working Happstack application using Heist as a template engine.