Quasiquotation of the Untyped Lambda Calculus

Tags: , , ,
May 9, 2024

After getting a taste of Template Haskell for my last post, I wanted to try out quasiquotation. This is all directly adapted from the original paper Why It’s Nice to be Quoted: Quasiquoting for Haskell and this blog post, but some details took a bit so I thought I’d write a short post walking through the main points. The full code can be found at this repo.

Quasiquotation isn’t something unique to Haskell. My understanding is that its history dates back to Lisp. I actually first encountered it in R, a language that traces its lineage to Lisp, where it is used extensively in the tidyverse package ecosystem.

The main idea of quasiquotation as I’ll be using it here is allowing the embedding of a DSL, in this case the untyped lambda calculus, that allows not only the parsing of literal expressions but interaction with Haskell terms. It is probably simplest to just see an example:

{-# LANGUAGE QuasiQuotes #-}

module Main where

import Control.Exception (assert)
import Eval
import Lambda

main :: IO ()
main =
  do
    let scc = [ex|λn. λs. λz. s (n s z)|]
    let plus = [ex|λm. λn. λs. λz. m s (n s z)|]

    let zero = [ex| λs. λz. z|]
    let one = full_beta [ex| $e:scc $e:zero |]
    let two = full_beta [ex| $e:scc $e:one |]
    let three = full_beta [ex| $e:scc $e:two |]

    print $ assert (one == [ex| λs. λz. s z |]) one
    print $ assert (three == full_beta [ex| $e:plus $e:one $e:two|]) three

which will print:

Lam (V "s") (Lam (V "z") (App (Var (V "s")) (Var (V "z"))))
Lam (V "s") (Lam (V "z") (App (Var (V "s")) (App (Var (V "s")) (App (Var (V "s")) (Var (V "z"))))))

Here the examples are some arithmetic with Church-encoded numerals. I think the appeal is pretty immediately obvious. It is much nicer to be able to write our lambda calculus terms in this recognizable format, and the second example shows how we can embed Haskell terms as well. In fact, quasiquotation can even be used within patterns! It should also be noted that all of this is happening at compile time.

So, what all was required to set this up? It really is not that bad. Here are the types for the lambda calculus:

data Var
  = V String
  | AV String
  deriving (Show, Eq, Data)

data Exp
  = Var Var
  | Lam Var Exp
  | App Exp Exp
  | AE String
  deriving (Show, Data, Eq)

The two additions here from a usual definition are the constructors AV and AE, which will be used for the antiquotation that allows interaction with Haskell variables.

One nice thing about quasiquotation is that we’ll be able to use an ordinary parser:

import Text.Megaparsec (Parsec, between, eof, errorBundlePretty, many, parse, satisfy, try, (<|>))
import Text.Megaparsec.Char (alphaNumChar, char, space, string)
import qualified Text.Megaparsec.Char.Lexer as L

-- Parsing boilerplate
type Parser = Parsec Void String

lexeme :: Parser a -> Parser a
lexeme = L.lexeme space

parseIO :: Parser a -> String -> IO a
parseIO p str =
  case parse p "" str of
    Left err -> fail $ errorBundlePretty err
    Right a -> return a

topLevel :: Parser a -> Parser a
topLevel p = space *> p <* eof

-- untyped lambda calculus parser, including antiquotation
ident :: Parser String
ident = lexeme $
  do
    c <- satisfy $ ap ((&&) . isLower) (/= 'λ')
    cs <- many (alphaNumChar <|> char '_' <|> char '\'')
    return (c : cs)

var :: Parser Var
var = (V <$> ident) <|> (AV <$> (string "$v:" >> ident))

pexp :: Parser Exp
pexp = foldl1 App <$> many aexp

aexp :: Parser Exp
aexp = 
    (try $ Var <$> var)
    <|>
    do lexeme $ (char '\\' <|> char 'λ')
       v <- var
       lexeme $ char '.'
       Lam v <$> pexp
    <|>
    (between (lexeme $ char '(') (lexeme $ char ')') pexp)
    <|>
    (AE <$> (string "$e:" >> ident))

This is all pretty straightforward, though anyone who follows me on Twitter may recognize this as the point where 'λ' being counted as lowercase caused me some difficulties.

Lastly, we need to be able define when exactly we want to be able to interact with Haskell terms, which is precisely in this case when we encounter the AV and AE constructors:

antiVarE :: Var -> Maybe ExpQ
antiVarE (AV v) = Just $ varE $ mkName v
antiVarE _ = Nothing

antiExpE :: Exp -> Maybe ExpQ
antiExpE (AE v) = Just $ varE $ mkName v
antiExpE _ = Nothing

antiVarP :: Var -> Maybe PatQ
antiVarP (AV v) = Just $ varP $ mkName v
antiVarP _ = Nothing

antiExpP :: Exp -> Maybe PatQ
antiExpP (AE v) = Just $ varP $ mkName v
antiExpP _ = Nothing

And finally, we construct the quasiquoter:

ex :: QuasiQuoter
ex =
  QuasiQuoter
    { quoteExp = \str -> (runIO $ parseIO (topLevel pexp) str) >>= dataToExpQ (const Nothing `extQ` antiVarE `extQ` antiExpE),
      quotePat = \str -> (runIO $ parseIO (topLevel pexp) str) >>= dataToPatQ (const Nothing `extQ` antiVarP `extQ` antiExpP),
      quoteDec = undefined,
      quoteType = undefined
    }

Here we leave quoteDec and quoteType undefined. These would allow quasiquoters for types and declarations which we don’t have a use for here, but might make sense for instance in a typed lambda calculus that uses Haskell types.

Recall that I mentioned that we can also use quasiquoters in patterns. Here’s an example of what that looks like:

toChurch :: Int -> Exp
toChurch 0 = [ex| λs. λz. z|]
toChurch n = full_beta [ex| $e:scc $e:prev |]
  where
    scc = [ex|λn. λs. λz. s (n s z)|]
    prev = toChurch $ n - 1

fromChurch :: Exp -> Maybe Int
fromChurch [ex| λs. λz. z|] = Just 0
fromChurch [ex| λs. λz. s $e:e|] = (+1) <$> fromChurch [ex| λs. λz. $e:e|]
fromChurch _ = Nothing

Without quasiquotation, this would be much more difficult to read:

toChurch :: Int -> Exp
toChurch 0 = Lam (V "s") $ Lam (V "z") (Var $ V "z")
toChurch n = full_beta $ App scc prev
  where
    scc = Lam (V "n") $ Lam (V "s") $ Lam (V "z") $ App (Var $ V "s") (App (App (Var $ V "n") (Var $ V "s")) (Var $ V "z"))
    prev = toChurch $ n - 1

fromChurch :: Exp -> Maybe Int
fromChurch (Lam (V "s") (Lam (V "z") (Var (V "z")))) = Just 0
fromChurch (Lam (V "s") (Lam (V "z") (App (Var (V "s")) e))) = (+1) <$> fromChurch (Lam (V "s") (Lam (V "z") e))
fromChurch _ = Nothing

My main difficulties in writing this post were not really technical, but in finding documentation. Hopefully this will help the next person that comes along!