Search code examples
haskellsplicehaskell-snap-frameworkheist

Bind splice to tag in Heist


I want to use a website as a working example in order to help learn Haskell. I'm trying to follow the Heist tutorial from the Snap website, and display the result of a factorial function in a web page.

I can get the example function defined "server side" without the compiler complaining, but I cannot figure out how to bind the function to a tag which I can then place into the HTML. Specifically, this part works fine (in, say, Site.hs):

factSplice :: Splice Snap
factSplice = do
    input <- getParamNode
    let text = T.unpack $ X.nodeText input
        n = read text :: Int
    return [X.TextNode $ T.pack $ show $ product [1..n]]

But the really important part - how to evaluate this function as part of a web page (such as how to bind it to a tag like < fact />) - is cryptic. The instructions say to drop:

bindSplice "fact" factSplice templateState

somewhere in the code. But this alone is not sufficient. This statement is not an expression (stuff = bindSplice...), so it is not clear how or where to put it in the code. Moreover, it is not at all clear where "templateState" is supposed to come from. It almost seems like "templateState" is supposed to be a placeholder for default values like emptyTemplateState or defaultHeistState, but these both appear to have been deprecated years ago, and the latest version of Heist (0.14) does not recognize them.

MightyByte has commented on this kind of question several times in 2011, but the answers all gloss over exactly the confusing part, i.e. how to actually get data into a web page. Can anyone help?

-- UPDATE --

Thank you very much, mightybyte! Your explanation and some cursory poking around in the source code cleared up a lot of confusion, and I was able to get the factorial example from the Snap website tutorial working. Here is my solution - I'm a complete n00b, so apologies if the explanation seems pedantic or obvious.

I more or less used the addConfig approach that mightybyte suggested, simply copying the implementation of addAuthSplices from SpliceHelpers.hs. I started with the default project via "snap init", and defined a function addMySplices in Site.hs

addMySplices :: HasHeist b => Snaplet (Heist b) -> Initializer b v ()
addMySplices h = addConfig h sc
    where
      sc = mempty & scInterpretedSplices .~ is
      is = do
           "fact" ## factSplice

This uses lenses to access the fields of the SpliceConfig neutral element mempty, so I had to add Control.Lens to the dependencies in Site.hs, as well as Data.Monoid to put mempty in scope. I also changed the type signature of the factorial splice to factSplice :: Monad n => I.Splice n, but the function is otherwise unchanged from its form in the Heist tutorial. Then I put a call to addMySplices in the application initializer, right next to addAuthSplices in Site.hs

app :: SnapletInit App App
...
addAuthSplices h auth
addMySplices h
...

which results in factSplice being bound to the tag <fact>. Dropping <fact>8</fact> into one of the default templates renders 40320 on the page, as advertised.

This question from about a year ago contains a superficially similar solution, but does not work with the latest version of Heist; the difference is that some fields were made accessible through lenses instead of directly, which is explained on the Snap project blog in the announcement of Heist 0.14. In particular, hcCompliedSplices has been completely redefined - there's even a friendly warning about it in Types.hs.


Solution

  • If you look at the top level Heist module, you'll see the initHeist function. As the documentation points out, this is the main initialization function. And you are supposed to pass it all of your templates and splices. A look at the type signature tells us these are all bundled together in the HeistConfig data type.

    initHeist :: Monad n => HeistConfig n -> EitherT [String] IO (HeistState n)
    

    This gives you back a HeistState, which is what you pass to renderTemplate when you want to render a template. Also, HeistConfig contains a SpliceConfig field for all your splices.

    But this is all the low level interface. Heist was designed to have no dependencies on Snap. It is a template library and can work with any web framework. (Or even standalone with no web framework at all for things like generating HTML email.) If you're using Heist with Snap, you'll probably want to use the convenience stuff that we supply which handles the details of calling initHeist, renderTemplate, etc.

    For a working example of this, your best bet is to look at the project template that you get when you do "snap init". You can see that code on github here. If you look in Application.hs, you'll see that there's a line including the Heist snaplet in the application data structure. Then, if you look at the bottom of Site.hs, you'll see that there's a call to heistInit. This function is defined by the heist snaplet here. The snaplet takes care of the initialization, state management, on-the-fly template reloading, etc for you.

    So, to put this all together...from the Heist API described above, we see that we need to define our splices inside HeistConfig. But our application will interact with all this stuff mostly through the snaplet's API. So if we look in the heist snaplet API for something involving HeistConfig/SpliceConfig, we find two relevant functions:

    heistInit' :: FilePath -> HeistConfig (Handler b b) -> SnapletInit b (Heist b)
    addConfig :: Snaplet (Heist b) -> SpliceConfig (Handler b b) -> Initializer b v ()
    

    These type signatures suggest two ways of defining your splices. You could use heistInit' instead of heistInit and pass your HeistConfig to it up front. Or you could add your splices afterwards by calling addConfig with the resulting Snaplet (Heist b).