This document is a literate haskell file.
{-# LANGUAGE LambdaCase #-}
module MyLambdaCalculus where
import Text.Parsec (char, eof, many1, runParser, satisfy, sepBy1, (<|>))
type Name = String
Lambda calculus consists of three terms:
data Term =
Var Name
| Lam Name Term
| App Term Term
deriving (Show)
-
Var
(Variable) is a character or string representing a parameter or mathematical/logical value. -
Lam
(Abstraction) is a function definition. -
App
(Application) applying a function to an argument.
Instead of writting the term manually, we can parse the standard syntax:
parse :: String -> Term
parse s = let Right term = runParser (termParser <* eof) () "lambda" s in term
where
termParser = reorderApp <$> (parensParser <|> lamParser <|> varParser) `sepBy1` char ' '
parensParser = char '(' *> termParser <* char ')'
lamParser = Lam <$> (char 'λ' *> varParser <* char '.' >>= \(Var name) -> pure name) <*> termParser
varParser = Var <$> many1 (satisfy (not . flip elem "()λ. "))
reorderApp (term : terms) = case terms of
(x : xs) -> foldl App (App term x) xs
[] -> term
Church encoding
Boolean:
true = parse "λx.λy.x"
false = parse "λx.λy.y"
and' = parse "λp.λq.p q p"
or' = parse "λp.λq.p p q"
not' = parse "λp.λa.λb.p b a"
Omega term can’t be beta-reduced:
omega = parse "(λx.x x) (λx.x x)"
Sum types, see The visitor pattern is essentially the same thing as Church encoding:
circle = parse "λradi.λc.λr.c radi"
rectangle = parse "λw.λh.λc.λr.r w h"
area = parse "λs.s (λradi.mul pi (sqrt radi)) (λw.λh.mul h w)"
example_circle = App circle (Var "10")
Evaluation strategy
References:
Applicative order
The argument to a function is evaluated before the function is applied:
-
(λx.λy.x y) ((λx.x) y) ((λx.x) x)
-
(λx.λy.x y) y x
-
y x
Can also be refered to as call by value
.
Normal order
The argument to a function are substituted into the body before the function evaluation:
-
(λx.λy.x y) ((λx.x) y) ((λx.x) x)
-
((λx.x) y) ((λx.x) x)
-
y x
call by name
means the argument is left un-touched, and it may be evaluated multiple times if it is used more than once in the function body. call by need
means the argument is passed as a thunk, and it may evaluate once when used.
Differences
-
Applicative order is the most common, it can be refered to as
strict
. Though it prevents the implementation of custom control flow, e.g.if
function needs a special case to by-pass then/else clause evaluation. Diverging argument like omega can’t be used. -
Normal order does not need special case for control flow which can be implemented in the language.
call by value
can be refered to aslazy
.
Normal order example:
Alpha conversion
Lambda term’s variables may need to be renamed to avoid collision. Multiple strategies exist, see bound, and here is a naive implementation:
-- | Convert conflicting variable name (based on code by Renzo Carbonara)
sub :: Name -> Term -> Term -> Term
sub name term = \case
Var varName -> if name == varName then term else Var varName
App t1 t2 -> App (convert t1) (convert t2)
Lam varName body
| -- varName shadows name
name == varName ->
Lam varName body
| -- varName conflicts
isFree varName term ->
let newName = freshName "x" term
newBody = sub varName (Var newName) body
in Lam newName (convert newBody)
| otherwise ->
Lam varName (convert body)
where
convert = sub name term
isFree n e = n `elem` freeVars e
freeVars = \case
Var n -> [n]
App f x -> freeVars f <> freeVars x
Lam n b -> filter (/= n) (freeVars b)
freshName s e
| isFree s e = freshName ('x' : s) e
| otherwise = s
Beta reduction
Lambda term’s variables can be replaced on application:
betaReduce :: Term -> Term
betaReduce = \case
App t1 t2 -> case betaReduce t1 of
Lam name body -> betaReduce (sub name t2 body)
_ -> App t1 t2
Lam name body -> Lam name (betaReduce body)
term -> term
main = do
putStrLn "True and !True are equivalent:"
print $ betaReduce true
print $ betaReduce (App not' false)
putStrLn ""
putStrLn "Area of a circle of radius 10:"
print $ betaReduce (App area (App circle (Var "10")))
putStrLn "Area of a rectangle of size 5x10:"
print $ betaReduce (App area (App (App rectangle (Var "5")) (Var "10")))
To evaluate the file: nix-shell -p ghcid -p “haskellPackages.ghcWithPackages (p: [p.markdown-unlit p.parsec])” –command ‘ghcid –test=:main –command “ghci -pgmL markdown-unlit” lambda-calculus.lhs’
hello world
Here is a demo in javascript:
// "hello world" using pure lambda
// * Church numeral
// We encode number using the church numeral representation:
let zero = f => x => x
let one = f => x => f(x)
let two = f => x => f(f(x))
let three = f => x => f(f(f(x)))
// Basic algebra:
let incr = x => f => s => f(x(f)(s))
let add = x => y => x(incr)(y)
let mul = x => y => f => x(y(f))
let exp = x => y => y(x)
// Numbers definition:
let four = incr(three)
let five = add(two)(three)
let seven = incr(mul(three)(two))
let eight = exp(two)(three)
let nine = exp(three)(two)
let eleven = incr(incr(nine))
let hundred = mul(exp(two)(two))(exp(five)(two))
// ASCII letters:
let H = mul(nine)(eight)
let e = incr(hundred)
let l = mul(four)(exp(three)(three))
let o = mul(three)(incr(mul(four)(nine)))
let spc = exp(two)(five)
let M = mul(seven)(eleven)
let C = incr(mul(two)(mul(three)(eleven)))
// * Sum type encoding (see: https://www.haskellforall.com/2021/01/the-visitor-pattern-is-essentially-same.html)
// A list is defined as either Nil, or Cons(a, List)
let nil = c => n => n
let cons = a => l => c => n => c(a)(l)
// * Text buffer
let mem = cons(H)(cons(e)(cons(l)(cons(l)(cons(o)(cons(spc)(cons(M)(cons(C)(cons(H)(nil)))))))))
// * Y-combinator
let Y = f => (x => f(z => x(x)(z))) (x => f(z => x(x)(z)))
// Let bindings are not recursive, thus we use a combinator to enable self reference
let map_rec = map => f => l => l(x => xs => cons(f(x))(map(f)(xs)))(nil)
let map = Y(map_rec)
// The entry point:
let main = decode_list => decode_value => decode_list(map(decode_value)(mem))
// The main function is defined in pure lambda calculus, using only:
// - `x`: Variable
// - `λx.x`: Abstraction
// - `x x`: Application
// In particular, the function doesn't uses any number or operator.
//
// Helpers to decode our lambda program:
let decode_list = acc => l => l(x => xs => decode_list(acc.concat([x]))(xs))(acc)
let decode_number = n => n(x => x + 1)(0)
console.log(String.fromCharCode(...main(decode_list([]))(decode_number)))
And using this script:
# Make the final demo by applying let transformation
lines = open("lambda.js").readlines()
lets = [(e.split()[1], e.strip().split(" = ")[1]) for e in filter(lambda s: s.startswith("let "), lines)]
def desugar(xs):
(k, v) = xs[0]
if k == "main":
return v
return "(" + k + " => " + desugar(xs[1:]) + ")(" + v + ")"
print("let main = " + desugar(lets))
body = False
for line in lines:
if body and line != "\n":
print(line, end="")
if line.startswith("let main"):
body = True
We can craft this demo:
let main = (zero => (one => (two => (three => (incr => (add => (mul => (exp => (four => (five => (seven =>
(eight => (nine => (eleven => (hundred => (H => (e => (l => (o => (spc => (M => (C => (nil =>
(cons => (mem => (Y => (map_rec => (map => decode_list => decode_value => decode_list(map(decode_value)(mem)))
(Y(map_rec)))(map => f => l => l(x => xs => cons(f(x))(map(f)(xs)))(nil)))
(f => (x => f(z => x(x)(z))) (x => f(z => x(x)(z)))))
(cons(H)(cons(e)(cons(l)(cons(l)(cons(o)(cons(spc)(cons(M)(cons(C)(cons(H)(nil)))))))))))
(a => l => c => n => c(a)(l)))(c => n => n))(incr(mul(two)(mul(three)(eleven)))))(mul(seven)(eleven)))
(exp(two)(five)))(mul(three)(incr(mul(four)(nine)))))(mul(four)(exp(three)(three))))(incr(hundred)))
(mul(nine)(eight)))(mul(exp(two)(two))(exp(five)(two))))(incr(incr(nine))))(exp(three)(two)))
(exp(two)(three)))(incr(mul(three)(two))))(add(two)(three)))(incr(three)))(x => y => y(x)))
(x => y => f => x(y(f))))(x => y => x(incr)(y)))(x => f => s => f(x(f)(s))))(f => x => f(f(f(x)))))
(f => x => f(f(x))))(f => x => f(x)))(f => x => x)
// The main function is defined in pure lambda calculus, using only:
// - `x`: Variable
// - `λx.x`: Abstraction
// - `x x`: Application
// In particular, the function doesn't uses any number or operator.
//
// Helpers to decode our lambda program:
let decode_list = acc => l => l(x => xs => decode_list(acc.concat([x]))(xs))(acc)
let decode_number = n => n(x => x + 1)(0)
console.log(String.fromCharCode(...main(decode_list([]))(decode_number)))
See the mockingbird book