Original article
MuniHac 2018: Keynote: Beautiful Template Haskell - YouTube

A brief introduction to typed template Haskell

The only way you can make a Code value is by quoting.

The only way you can use a Code value is by splicing.

Why typed template programming is beautiful is that there are types.

Quote and Slice interact in beautifuls to generate programs.

Glossary

1
2
3
4
staging programs
    Writing programs using template haskell.

stage polymorphic programs
1
2
3
typed template haskell
    All about generating expressions in a
    principled manner.

quote

1
2
x :: Code Int
x = [| 2 + 3 |]

Takes an expression 2 + 3 and makes a value of type Code Int (code of int) and the way to think of this is it makes a syntax tree.

1
2
"+" -> 2
"+" -> 3
+---+     +---+
| + | --> | 2 |
+---+     +---+
  |
  |
  v
+---+
| 3 |
+---+

splice

1
2
y :: Code Int
y = [| 2 + $(x) |]

Take things you have already quoted and insert them into other quotes.

This takes x, which was defined above and puts it into this expression.

typed template programming

This will not work.

1
2
x :: Code String
x = [| "string" |]
1
2
y :: Code Int
y = [| 1 + $(X) |]

hygiene

1
2
foo = [| \x -> $(x) |]
--        1      0

This is not going to work. Why? The information flows in the wrong direction.

Needs to evaluate x while the splice is being compiled and at that point x is not going to be bound.

So, assign a level to each variable. When you enter the splice, you decrease the level.

1
2
foo = [| \x -> $([| x |])
--        1         1

Quote the variable quote and then it’s ok. By inserting the quotes we make sure the variables have the same level.

An intuitive way to think about this is while we don’t know what x will be at stage zero, at least we do know that there will be a variable called x at stage 1.

Principle of Levels
A variable can only be used at the level it is bound.

Cross-Stage persistence - Serialisation Based

This is the 1st way to persist values.

1
2
3
double :: Int -> Code In
double x = [| x * 2 |]
--     0      1

x is bound in level 0 and used in level 1, which is in the future.

This program is not level correct so it should be rejected.

Because we can persist integers (given the code at runtime, we can produce the code that produces that integer)

1
2
3
double :: Int -> Code Int
double x = [| $(lift x) * 2 |]
--     0             0

What lift does, is it takes the integer and it produces code that produces that integer and then we get a level-correct program.

Refined Principle of Levels
A variable can only be used at the level it is bound unless we can persist it.

The main thing you can’t persist is functions, because how can you take an arbitrary function that lives at runtime and produce code that generates that function? It’s not in general possible.

But with an integer, you can just create the syntax tree that has the integer in it.

Cross-Stage Persistence - Path Based

This is the 2nd way to persist values.

1
2
3
4
5
6
7
8
9
module M where

succ :: Int -> Int
succ x = x + 1
-- 0

codeSucc :: Code (Int -> Int)
codeSucc = [| succ |]
--            1

Persist the value of succ to the future for when we generate the program.

This is possible because top level identifiers will still be bound when you run this program later on.

If something is bound at the top level then you can also persist it.

A specialised function

1
2
3
power5 :: Int -> Int

power5 k = k * k * k * k * k

Generalise the function – an abstract function

These do not work as well as you think. The optimiser does not get you the original function

1
2
3
4
5
power :: Int -> Int -> Int
power 0 k = 1
power n k = k * power (n - 1) k

power5' = power 5

<interactive>:1:1: error:
    Variable not in scope: power :: Int -> Int -> Int
1
2
3
4
5
6
power :: Int -> Int -> Int

power 0 = \k -> 1
power n = \k -> k * power (n-1) k

power5' = power 5

Using template haskell, you can get the original function

1
2
3
power :: Int -> Code (Int -> Int)
power 0 = [| \k -> 1 |]
power n = [| \k -> k * $(power (n-1)) k |]

<interactive>:1:17: error:
    Not in scope: type constructor or class β€˜Code’

<interactive>:2:12: error: parse error on input β€˜|’

<interactive>:3:12: error: parse error on input β€˜|’

Staging programs

1
2
3
power :: Int -> Code (Int -> Int)
power 0 = [| \k -> 1 |]
power n = [| \k -> k * $(power (n-1)) k |]

<interactive>:1:17: error:
    Not in scope: type constructor or class β€˜Code’

<interactive>:2:12: error: parse error on input β€˜|’

<interactive>:3:12: error: parse error on input β€˜|’

If we statically know an integer, then we’re going to produce code specialised to that integer that takes k and raises it to the nth power.

The first argument is the thing that we exactly know.

The only way we know how to make code values is by quotation.

The splice $(power (n-1)) is where most of the computation happens.

In order to evaluate what this splice evaluates to we need to evaluate the recursive call to power. As we do that recursively, we unroll the function recursively.

TODO example

https://github.com/yesodweb/yesod/blob/931caaa2c01c4e3515f0d12f248ddc6bdfad5ccb/yesod-bin/Scaffolding/Scaffolder.hs

  • .hsfiles
1
2
3
4
5
6
7
8
backendBS :: Backend -> S.ByteString
backendBS Sqlite = $(embedFile "hsfiles/sqlite.hsfiles")
backendBS Postgresql = $(embedFile "hsfiles/postgres.hsfiles")
backendBS PostgresqlFay = $(embedFile "hsfiles/postgres-fay.hsfiles")
backendBS Mysql = $(embedFile "hsfiles/mysql.hsfiles")
backendBS MongoDB = $(embedFile "hsfiles/mongo.hsfiles")
backendBS Simple = $(embedFile "hsfiles/simple.hsfiles")
backendBS Minimal = $(embedFile "hsfiles/minimal.hsfiles")