Looping and Control Flow in Heist
One of the most frequent questions I get about Heist is how to do loops or control flow in Heist templates. I usually respond that we've tried to avoid introducing these constructs into templates to encourage a better separation between view and model/controller. In this post I want to discuss the issue in more detail so that my thoughts are stored and I can refer people here in the future.
Since Haskell is pure and has a strong static type system, the first thing to think about when discussing this question is what types will be involved. Since templates are processed at runtime, they do not have access to Haskell's type system. This puts us square in the middle of byte-land. No generalized looping over things like the list of registered users or recent posts. No type-safe evaluation of arbitrary expressions. It is my contention that you're better off solving these problems with a language designed for that purpose...i.e. in Haskell splices, and not in HTML templates.
Let's continue the example of displaying blog posts used previously. We developed a function
Notice that this function is no longer a splice. It's a function that takes a Post as input and generates a splice as output. Now we can use the new mapSplices function introduced in Heist 0.5.1.0 to render a list of posts.
That was easy. Now one more step gets us a splice that we can bind to the <recentPosts/> tag and use in our templates.
This splice can be used in a template exactly the same way we did before. The only difference is that the view we passed into the splice is now applied to each post in the list and the results are concatenated.
I specialized this function to the Application monad which will contain functionality specific to your application that provides the necessary context for the getRecentPosts function. I left out the details of that function because those details aren't something the template author should worry about. And here we get to the main point. The core of Heist's keep-application-logic-out-of-the-view philosophy boils down to the idea that you're better off creating a domain-specific set of query functionality than exposing too much generality to the templates.
One likely avenue of domain-specific query generalization might be to look at only the posts with a certain tag. We might do something like this.
Hopefully this demonstrates how templates can interact with looping and control flow defined in Haskell code using the gorgeous syntax we all know and love, and how we can leverage Haskell's powerful abstraction constructs to create nice domain-specific markup languages that are easy for designers to incorporate into their web designs. In the next post I'll look more closely into the abstractions available exclusively in templates.
EDIT: To clarify, the splices in this post will be used in exactly the same way the <post> splice was used in the last post. The different splice just changes the context in which the passed-in parameters are used. For example:
Since Haskell is pure and has a strong static type system, the first thing to think about when discussing this question is what types will be involved. Since templates are processed at runtime, they do not have access to Haskell's type system. This puts us square in the middle of byte-land. No generalized looping over things like the list of registered users or recent posts. No type-safe evaluation of arbitrary expressions. It is my contention that you're better off solving these problems with a language designed for that purpose...i.e. in Haskell splices, and not in HTML templates.
Let's continue the example of displaying blog posts used previously. We developed a function
postSplice :: Splice m
(don't worry about the 'm' for now) that our templates could use to render a single post using whatever markup the template designer specified. Now we want to use a looping structure to reuse that code for a list of posts. Where does this list come from? It comes from Haskell code (not templates), so it's logical to do our looping abstraction in Haskell. I would probably refactor the postSplice function as follows.renderPost :: Monad m => Post -> Splice m renderPost post = do runChildrenWithText [("postTitle", postTitle post) ,("postBody", postBody post)]
Notice that this function is no longer a splice. It's a function that takes a Post as input and generates a splice as output. Now we can use the new mapSplices function introduced in Heist 0.5.1.0 to render a list of posts.
renderPosts :: Monad m => [Post] -> Splice m renderPosts = mapSplices renderPost
That was easy. Now one more step gets us a splice that we can bind to the <recentPosts/> tag and use in our templates.
recentPostsSplice :: Splice Application recentPostsSplice = getRecentPosts >>= renderPosts
This splice can be used in a template exactly the same way we did before. The only difference is that the view we passed into the splice is now applied to each post in the list and the results are concatenated.
I specialized this function to the Application monad which will contain functionality specific to your application that provides the necessary context for the getRecentPosts function. I left out the details of that function because those details aren't something the template author should worry about. And here we get to the main point. The core of Heist's keep-application-logic-out-of-the-view philosophy boils down to the idea that you're better off creating a domain-specific set of query functionality than exposing too much generality to the templates.
One likely avenue of domain-specific query generalization might be to look at only the posts with a certain tag. We might do something like this.
postsWithTag :: Text -> Splice Application postsWithTag tag = getTaggedPosts tag >>= renderPosts postsWithTagSplice :: Splice Application postsWithTagSplice = do node <- getParamNode maybe (return []) postsWithTag $ getAttribute "tag" nodeThen with the appropriate splice binding, we can do this in our templates.
<postsWithTag tag="heist"/>Or, instead of getting the tag from an attribute of the param node, we could do this instead.
postsWithTagSplice :: Splice Application postsWithTagSplice = do tag <- lift $ getParam "tag" maybe (return []) postsWithTag tagIn the Snap template project, Application has a MonadSnap instance, so we can lift the getParam function into the splice's monad. This gets the tag from the HTTP request parameters of whatever request is being processed when <postsWithTag> is rendered. This makes it easy to tie into values gathered from a form or even specified manually in the query string.
Hopefully this demonstrates how templates can interact with looping and control flow defined in Haskell code using the gorgeous syntax we all know and love, and how we can leverage Haskell's powerful abstraction constructs to create nice domain-specific markup languages that are easy for designers to incorporate into their web designs. In the next post I'll look more closely into the abstractions available exclusively in templates.
EDIT: To clarify, the splices in this post will be used in exactly the same way the <post> splice was used in the last post. The different splice just changes the context in which the passed-in parameters are used. For example:
<recentPosts> <h1><postTitle/></h1> <div><postBody/></div> </recentPosts>
Comments
I am trying to follow what you have written, but I have trouble with the interaction of the templates, probably as I am still very much learning how the Haskell type system works.
If I understand correctly, the result from renderPosts will include multiple "postTitle"s and "postBody"s, which (if following along from the previous article) might live in a template file called post.tpl.
I am a bit lost as to how these end up getting integrated into a single output - it would help me greatly if you could sketch how, say, an index.tpl (containing a "posts" tag) and post.tpl were combined to create a single page with multiple posts drawn from post.tpl.
Thanks again!
addSplices [("recentPosts", recentPostsSplice)]
If you are using Heist directly, you would use the equivalent function bindSplices from the Heist API to set up your initial HeistState.
Once you have bound the splice, then whenever Heist encounters the tag in any of your templates, it runs the recentPostsSplice function and replaces the tag and all its children with the returned nodes. The renderPost function returns the child nodes (the h1 and div tags in the example at the end) after substituting the real post title and body in place of the postTitle and postBody tags. The renderPosts (plural) function just calls renderPost for every post in the list. Check out our Heist tutorial if you haven't already for more information.
I have defined a handler function which uses heistLocal, bindSplices, and render (which names a template file). Should I be able to modify just this handler to include addSplices? Given that I use two separate template files, do I need a separate call to render to get the application to look at the second template file, or does something else read that in? Perhaps a call to addTemplates in Snaplet?
I see that addSplices has the type [(T.Text, SnapletSplice b v)] -> Initializer b v () but it is not apparent to me how that initializer would fit with heistLocal and bindSplices (which involve HeistState types). Is there a relationship between Initializer and HeistState that I should be aware of, and how would I go about investigating it? (I've tried :i in ghci for both HeistState and Initializer, but neither seems to refer to the other).