Trying to Get Started With Heist
2023-08-30
Table of Contents
Introduction
The goal is to have a minimal Haskell project that uses Heist to render a HTML page that does:
- Load and precompile all templates
- Make a fake database call outside of any splice/template/Heist functions
- Apply the precompiled templates to data
I will be using compiled Heist where possible. I’d also like to use as few dependencies as possible. I don’t want to use map-syntax
and if I could skip lenses that would be great, although I don’t have high hopes for that. Unfortunately lenses are one of those things that many dependencies force upon you.
There’s a resources section at the bottom, listing which resources I used to get started.
Nix Issues
Let’s start by initializing the project: nix flake init -t fbrs#haskell
1 followed by nix flake update
. I’ll also add an .envrc
file use flake
in it. And now I need to download around 8GB of stuff and probably compile some things, since I’m on an M2 MacBook. I actually updated the flakes in my Nix template repository now so the next time someone uses them there’s less downloading and building.
パソコンを待っているながら、日本語の練習すると思う。I’ll kill some time and do some Japanese vocab reviews.
I should have stopped the time, it’s still compiling stuff. Must have been 10min at least. I’ll play a round of Hearthstone. Still compiling after a satisfying win against a mech player. Back to vocab I guess.
I removed some cruft from the Haskell project files and upgraded the GHC version to 9.6.something but it’s still compiling. I’ve since moved on to YouTube shorts and I doubt I’ll write a single line of code before I got to start working in around 30min.
building ghc-lib-parser-9.6.2.20230523
:(
I’m clearly doing something wrong here. I think I should have never used this ghcWithPackages
since it requires me to recompile a whole lot of stuff and in any case my actual Haskell project files would still use whatever GHC version is currently the default in Nix. To be a bit more precise: the generated project.nix
is a derivation for my Haskell project. I inherit the Haskell environment in my dev shell through inputsFrom = [project.env];
. If I now use (haskell.packages.ghc96.ghcWithPackages (hpkgs: with hpkgs; [ ormolu ]))
in my buildInputs
for my dev shell I’m setting myself up for confusion since the GHC used in my dev shell won’t be the same as the one used by my project. Meaning the Correct Thing To Do™ here is to override the GHC version used in my project and NOT use ghcWithPackages
. The Haskell tools I’m installing (hlint, fast-tags, …) will still use whatever GHC version their compiled against, but that’s fine.
Heist
I’ve removed ghcWithPackages
from the template and the starter project for Heist. I think now everything is working. In retrospect, I could have probably skipped some 30–45min of compilation had I realized this error earlier.
I created two .tpl
files, one for the base template and another for the child template that shows a dynamic variable called <body-greeting />
. I’m already kind of stuck right now. I should now set up the load
function from the “Compiled Splices” tutorial, I guess. I really hate lenses. In the snippet below they start with mempty
and then add stuff to it using lenses. Let me guess, the actual data type isn’t exported? I get the idea. They can refactor the internal layout of that data structure and thanks to lenses no one needs to update their code. Using Hoogle scLoadTimeSplices
leads to Heist.Internal.Types
so lenses it is. Which lens package do I need though? I vaguely remember there being different generations of lens libraries or library families.
tmap <- runExceptT $ do
let sc = mempty & scLoadTimeSplices .~ defaultLoadTimeSplices
& scCompiledSplices .~ splices
& scTemplateLocations .~ [loadTemplates baseDir]
ExceptT $ initHeist $ emptyHeistConfig & hcNamespace .~ ""
& hcErrorNotBound .~ False
& hcSpliceConfig .~ sc
I added Lens.Micro.Platform
just because it looked like it’s maintained and reasonably batteries-included. GitHub Copilot helped me write:
let heistConfig = mempty & scLoadTimeSplices .~ defaultLoadTimeSplices
which seems to work, so I’ll just continue cobbling something together. I guess I don’t hate lenses after all. I’m struggling a bit with HeistState m
. m
is the runtime monad, which I guess would be IO
for me. It just feels wrong to now use C.Splice (HeistState IO)
when I just want to return a hardcoded string. I’m sure there’s something I can do about that. I did remember that there’s a whole yieldPure*
family of functions.
bodyGreetingSplice :: _
bodyGreetingSplice = do
C.yieldPureText "Hello, world!"
This gives me DList (Chunk n)
as the returned type. And that’s kind of close to the type of Splice
, which is type Splice n = HeistT n IO (DList (Chunk n))
according to the Github tutorial. I can’t currently see a way to avoid having all these little splice functions reference IO. I’ve already caved and am now using lenses, but can I avoid map-syntax
? Unfortunately, let splices = Map.fromList [ ("body-greeting", bodyGreetingSplice) ]
is rejected. Whatever, I’ll just use Data.Map.Syntax
. This isn’t the problem I’m trying to solve anyway.
Next roadblock: the tutorial does something like
let runtime = fromJust $ C.renderTemplate hs "index"
builder <- evalStateT (fst runtime) 2
They’re precompiling the index
template and then they’re dynamically rendering the pre-compiled template with the value 2
. So I guess I need to:
- pre-compile body
- pre-compile index
- dynamically render index and insert body (which I might need to turn into a bytestring first?)
I still don’t really get it. It doesn’t click, you know? I just skipped ahead and compiled and printed one of my templates. It printed:
"<!DOCTYPE HTML>\n<html>\n<head>\n <meta charset='utf-8' />\n <title>App</title>\n <meta name='viewport' content='width=device-width, initial-scale=1' />\n</head>\n<body>\n <h1>Hello!</h1>\n <apply-content></apply-content>\n</body>\n</html>\n"
That’s from
case eitherHeistState of
Left err ->
putStrLn $ "Heist init failed: " ++ show err
Right heistState -> do
case C.renderTemplate heistState "body" of
Nothing -> do
putStrLn "Body not found!"
Just (bodyBuilder, _) -> do
case C.renderTemplate heistState "index" of
Nothing -> do
putStrLn "Index not found!"
Just (docBuilder, _) -> do
b <- docBuilder
let s = toByteString b
print s
Notice I’m not using the body-greeting at all. I just remembered something though! I can modify the list of splices before rendering and I think that’s a major part of working with compiled splices. Essentially, within the scope of a certain template, I can just rebind apply-content
?
I don’t get where callTemplate
gets its lookup path from. It gets the Heist state (see here) but I thought IO
was serial, so if I call callTemplate
before initializing the state… how does it know where my templates live?
This is by far the most difficult templating system I have ever encountered in my life. It’s truly mind bending. Like, what the hell is going on here? ChatGPT just hallucinates functions like initHeistCompiled
.
I was able to cobble something together with ChatGPT and Copilot that uses the reader monad for the view data. I precompile my splices when the app starts (goal 1), I can grab the reader monad data at runtime (goal 2) but I still don’t know how to bind the apply-content
tag (goal 3). I’ll have to ask on Stack Overflow.
And here’s my SO question. This is the first time in my life I need to ask on SO how to do the most basic templating task. It’s a typical Haskell journey.
The Answer
I ended up answering my own question and the answer is embarassingly simple: just use the <apply />
tag. I thought that in compiled mode these basic template abstractions no longer applied, for some reason.
commit 8b65481098811e36a55260fbe812ffdf871672d1
Author: Florian Beeres <yuuki@protonmail.com>
Date: Wed Aug 30 21:53:20 2023 +0200
Fix it and answer the question
Use <apply /> to render the "body" template into the "index" template.
Meaning, bottom up, rather than top down.
diff --git a/app/Main.hs b/app/Main.hs
index b2b4fed..1f0e9d9 100644
--- a/app/Main.hs
+++ b/app/Main.hs
@@ -45,15 +45,7 @@ main = do
putStrLn $ "Heist init failed: " ++ show err
Right heistState -> do
-- 3. Apply the precompiled templates to data
- -- I need to replace <apply-content /> in index.tpl with the body-greeting splice, but I don't know how.
- -- I do not want to add the splice to "mainSplices" since in a server environment I would like to
- -- precompile the base splices once and then "add overlays" in each route handler
-
- -- This has <body-greeting /> inside of it, which we've already compiled and handled through "mainSplices"
- let (bodyTpl :: C.Splice IO) = C.callTemplate "body"
- -- ^-- stick this into the index.tpl template somehow, replacing the <apply-content /> tag
-
- case C.renderTemplate heistState "index" of
+ case C.renderTemplate heistState "body" of
Nothing -> do
putStrLn "Index not found!"
Just (docRuntime, _) -> do
diff --git a/app/body.tpl b/app/body.tpl
index 43c72c4..0d128df 100644
--- a/app/body.tpl
+++ b/app/body.tpl
@@ -1 +1,3 @@
-<p><body-greeting /></p>
+<apply template="index">
+ <p><body-greeting /></p>
+</apply>
Resources
- http://snapframework.com/docs/tutorials/compiled-splices
- https://github.com/kaol/heist-tutorial/blob/master/tutorial.md
This only works since I’ve set up some shortcuts for my Nix flake registry locally. The version that you’d have to use is nix flake init -t github:cidem/nix-templates#haskell
I think