Trying to Get Started With Heist
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.
Let’s start by initializing the project:
nix flake init -t fbrs#haskell1 followed by
nix flake update. I’ll also add an
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.
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.
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
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
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
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"
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
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
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.
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 <firstname.lastname@example.org> 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>
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