Exercise Solutions (Hidden)#

Each solution is inside a dropdown so you can reveal it when needed. The code blocks are runnable snippets (copy into a code cell if you want to execute them directly in this notebook).

Most snippets assume the corresponding chapter’s definitions are already in scope (e.g., Var, Lam, add, mul, Z). If you want a fully standalone run, copy the setup cells from that chapter first.

Reading guide

  • A line marked Formal proof is a general mathematical argument.

  • A line marked Finite check is only computational validation on a bounded range.

Exercise 1 (Lambda Basics)#

Solution (general proof + code)

Context We use the structural definition of free variables and the definition of alpha-equivalence.

Free variables (formal derivation) We use:

  1. \(FV(x)=\{x\}\)

  2. \(FV(t\,u)=FV(t)\cup FV(u)\)

  3. \(FV(\lambda x.\,t)=FV(t)\setminus\{x\}\)

Let \(t=\lambda x.\,x\,(y\,z)\). Then:

  1. \(FV(x\,(y\,z))=FV(x)\cup FV(y)\cup FV(z)=\{x,y,z\}\)

  2. \(FV(\lambda x.\,x\,(y\,z))=\{x,y,z\}\setminus\{x\}=\{y,z\}\)

Alpha-renaming (formal reasoning) Alpha-renaming replaces only occurrences bound by a specific binder. If the same name is rebound inside the body, the inner binder shadows the outer one and blocks renaming. Thus in

\[\lambda x.\,(\lambda x.\,x)\,x\]

only the last \(x\) (outside the inner lambda) is renamed when the outer binder is renamed.

Capture and freshness A renaming must be capture-avoiding. Example term:

\[\lambda x.\,x\,y\]

If we renamed the binder \(x\) to \(y\) naively, we’d obtain \(\lambda y.\,y\,y\), which is not alpha-equivalent: the free \(y\) got captured. A capture-avoiding alpha-rename instead picks a fresh name (e.g. \(y0\)) and produces \(\lambda y0.\,y0\,y\).

Free variables on sample terms

  • \(FV(\lambda x.\,\lambda y.\,x\,y)=\emptyset\)

  • \(FV(\lambda x.\,y\,x)=\{y\}\)

  • \(FV((\lambda x.\,x)\,y)=\{y\}\)

Code (using the Chapter 1 AST)

# Free variables
term = Lam("x", App(Var("x"), App(Var("y"), Var("z"))))
free_vars(term)  # {'y', 'z'}

# Alpha-renaming (outer binder only)
term = Lam("x", App(Lam("x", Var("x")), Var("x")))
renamed = alpha_rename(term, "w")
show(term)    # (\u03bbx. ((\u03bbx. x) x))
show(renamed) # (\u03bbw. ((\u03bbx. x) w))

# Capture-avoiding rename chooses a fresh name when needed
term = Lam("x", App(Var("x"), Var("y")))
renamed = alpha_rename(term, "y")
show(renamed)  # (\u03bby0. (y0 y))  (fresh binder, free y stays free)

# Free variables for the sample terms
samples = [
    Lam("x", Lam("y", App(Var("x"), Var("y")))),
    Lam("x", App(Var("y"), Var("x"))),
    App(Lam("x", Var("x")), Var("y")),
]
[free_vars(s) for s in samples]  # [set(), {'y'}, {'y'}]

Exercise 2 (Substitution and Reduction)#

Solution (general proof + code)

Context We use capture-avoiding substitution and beta-reduction.

Substitution rules (capture-avoiding)

  1. \(x[x:=u]=u\)

  2. \(y[x:=u]=y\), for \(y\neq x\)

  3. \((t\,u)[x:=v]=t[x:=v]\,u[x:=v]\)

  4. \((\lambda y.\,t)[x:=v]=\lambda y.\,t\) if \(y=x\)

  5. \((\lambda y.\,t)[x:=v]=\lambda y.\,t[x:=v]\) if \(y\neq x\) and \(y\notin FV(v)\)

  6. If \(y\in FV(v)\), alpha-rename first to avoid capture.

Beta-reduction of \((\lambda x.\,x)\,((\lambda y.\,y)\,z)\)

  1. \((\lambda x.\,x)\,((\lambda y.\,y)\,z)\)

  2. \(\to ((\lambda y.\,y)\,z)\)

  3. \(\to z\)

Normal-order vs applicative-order Let \(\Omega=(\lambda x.\,x\,x)(\lambda x.\,x\,x)\). Then:

  • Normal order: \((\lambda x.\,y)\,\Omega \to y\), terminates.

  • Applicative order: tries to reduce \(\Omega\) first, which diverges.

Why binder renaming is needed in subst(\lambda y. x, x := y) The replacement term \(y\) has free variable \(y\). If we substitute into \(\lambda y.\,x\) without renaming, we’d get \(\lambda y.\,y\) where that \(y\) is now bound. Correct capture avoidance renames the binder first:

\[ (\lambda y.\,x)[x:=y] \equiv (\lambda y0.\,x)[x:=y] = \lambda y0.\,y. \]

Weak-head vs full normalization Full normalization reduces under lambdas, so:

\[\lambda x. ((\lambda y. y)\,x) \to \lambda x. x.\]

Weak-head normalization stops at the outer lambda and leaves the body unreduced.

Code (using the Chapter 2 reducer)

# Reduce to z
z = Var("z")
term = App(Lam("x", Var("x")), App(Lam("y", Var("y")), z))
normalize(beta_step_normal, term)  # z

# Normalizes under normal order but diverges under applicative order
omega = App(Lam("x", App(Var("x"), Var("x"))), Lam("x", App(Var("x"), Var("x"))))
term = App(Lam("x", Var("y")), omega)

normalize(beta_step_normal, term)       # y
normalize(beta_step_applicative, term)  # diverges

# Substitution that requires alpha-renaming to avoid capture
term = Lam("y", Var("x"))
out = subst(term, "x", Var("y"))
show(out)  # (lambda y0. y)

# Weak-head normal form: do not reduce under Lam
def beta_step_normal_weak(t: Term):
    if is_redex(t):
        return subst(t.fn.body, t.fn.param, t.arg), True
    if isinstance(t, App):
        new_fn, changed = beta_step_normal_weak(t.fn)
        if changed:
            return App(new_fn, t.arg), True
        new_arg, changed = beta_step_normal_weak(t.arg)
        if changed:
            return App(t.fn, new_arg), True
        return t, False
    # Crucial: do not descend into Lam
    return t, False

t = Lam("x", App(Lam("y", Var("y")), Var("x")))
show(normalize(beta_step_normal, t))       # (lambda x. x)
show(normalize(beta_step_normal_weak, t))  # (lambda x. ((lambda y. y) x))

Exercise 3 (Church Booleans)#

Solution (general proof + code)

Context Church booleans are choice functions.

Definitions

\[\begin{split}\begin{aligned} \mathrm{TRUE} &\equiv \lambda t.\,\lambda f.\,t,\\ \mathrm{FALSE} &\equiv \lambda t.\,\lambda f.\,f,\\ \mathrm{NOT}\,p &\equiv p\,\mathrm{FALSE}\,\mathrm{TRUE},\\ \mathrm{OR}\,p\,q &\equiv p\,p\,q,\\ \mathrm{AND}\,p\,q &\equiv p\,q\,p,\\ \mathrm{IMPLIES}\,p\,q &\equiv p\,q\,\mathrm{TRUE}. \end{aligned}\end{split}\]

Proof that \(\mathrm{IMPLIES}\,p\,q = \mathrm{OR}(\mathrm{NOT}\,p)\,q\) Case split on \(p\):

  1. If \(p=\mathrm{TRUE}\):

    • \(\mathrm{IMPLIES}\,\mathrm{TRUE}\,q = q\)

    • \(\mathrm{OR}(\mathrm{NOT}\,\mathrm{TRUE})\,q = \mathrm{OR}\,\mathrm{FALSE}\,q = q\)

  2. If \(p=\mathrm{FALSE}\):

    • \(\mathrm{IMPLIES}\,\mathrm{FALSE}\,q = \mathrm{TRUE}\)

    • \(\mathrm{OR}(\mathrm{NOT}\,\mathrm{FALSE})\,q = \mathrm{OR}\,\mathrm{TRUE}\,q = \mathrm{TRUE}\)

So the two terms are extensionally equal for all \(p,q\).

XOR A convenient definition is:

\[\mathrm{XOR}\,p\,q := p\,(\mathrm{NOT}\,q)\,q.\]

If \(p\) is true, return \(\mathrm{NOT}\,q\); if \(p\) is false, return \(q\).

Code

NAND = lambda p: (lambda q: NOT(AND(p)(q)))
XOR = lambda p: (lambda q: p(NOT(q))(q))

# Truth table checks
for p in [TRUE, FALSE]:
    for q in [TRUE, FALSE]:
        # IMPLIES equivalence
        left = IMPLIES(p)(q)
        right = OR(NOT(p))(q)
        assert to_bool(left) == to_bool(right)

        # XOR truth table: True iff p != q
        xor_val = to_bool(XOR(p)(q))
        assert xor_val == (to_bool(p) != to_bool(q))

# AND commutativity + associativity
for p in [TRUE, FALSE]:
    for q in [TRUE, FALSE]:
        assert to_bool(AND(p)(q)) == to_bool(AND(q)(p))

for p in [TRUE, FALSE]:
    for q in [TRUE, FALSE]:
        for r in [TRUE, FALSE]:
            left = AND(AND(p)(q))(r)
            right = AND(p)(AND(q)(r))
            assert to_bool(left) == to_bool(right)

Exercise 4 (Propositional Logic)#

Solution (general proof + code)

Context By Curry-Howard, proofs are programs and propositions are types.

Formal proof sketches

  1. \(A \to (B \to A\land B)\) Use \(\to\)-introduction twice and \(\land\)-introduction once. Term: \(\lambda x\!:\!A.\,\lambda y\!:\!B.\,(x,y)\).

  2. \(A\land B \to A\) Use left projection from conjunction. Term: \(\lambda p\!:\!(A\land B).\,\mathrm{fst}(p)\).

  3. \(A \to (B \to A)\) Use \(\to\)-introduction twice and ignore the second argument (the K combinator).

  4. \(A\lor B \to B\lor A\) Use disjunction elimination (case analysis):

    • if \(p\) is an inl a, return inr a

    • if \(p\) is an inr b, return inl b

Code (type-checked)

A = TyVar("A")
B = TyVar("B")

# 1) A -> (B -> A and B)
ex1 = Lam("x", A, Lam("y", B, Pair(Var("x"), Var("y"))))

# 2) A and B -> A
ex2 = Lam("p", And(A, B), Fst(Var("p")))

# 3) A -> (B -> A)
ex3 = Lam("x", A, Lam("y", B, Var("x")))

# 4) A or B -> B or A
or_comm = Lam(
    "p",
    Or(A, B),
    Case(
        Var("p"),
        "a",
        Inr(Var("a"), B),
        "b",
        Inl(Var("b"), A),
    ),
)

print(show_type(type_of(ex1, {})))
print(show_type(type_of(ex2, {})))
print(show_type(type_of(ex3, {})))
print(show_type(type_of(or_comm, {})))

Exercise 5 (Church Numerals)#

Solution (general proof + code)

Context Church numerals represent iteration. We build predicates by observing how many times a function is applied.

is_zero (formal reasoning) Define \(\mathrm{is\_zero}\;n \equiv n\,(\lambda\_.\,\mathrm{FALSE})\,\mathrm{TRUE}\). If \(n=0\), no applications occur and TRUE is returned. If \(n>0\), the function is applied at least once and returns FALSE.

leq (formal reasoning) Define \(m\le n\) as \(\mathrm{is\_zero}(\mathrm{sub}\;m\;n)\). If \(m\le n\), subtraction reduces to \(0\); if \(m>n\), the result is non-zero.

Associativity of add (proof sketch by induction on \(c\)) We show \(\mathrm{add}(\mathrm{add}\;a\;b)\;c = \mathrm{add}\;a\;(\mathrm{add}\;b\;c)\).

  1. Base: \(c=0\), so \(\mathrm{add}(\mathrm{add}\;a\;b)\;0 = \mathrm{add}\;a\;b = \mathrm{add}\;a\;(\mathrm{add}\;b\;0)\).

  2. Step: assume true for \(c\). Then \(\mathrm{add}(\mathrm{add}\;a\;b)(S\,c)=S(\mathrm{add}(\mathrm{add}\;a\;b)\;c)=S(\mathrm{add}\;a\;(\mathrm{add}\;b\;c))=\mathrm{add}\;a\;(\mathrm{add}\;b\;(S\,c))\).

Distributivity of mul over add (proof sketch by induction on \(c\)) We show \(a\cdot(b+c) = a\cdot b + a\cdot c\). This is standard by induction on \(c\), using the recursion law \(\mathrm{mul}\;a\;(S\,c) = \mathrm{add}(\mathrm{mul}\;a\;c)\;a\).

Finite check vs general proof

  • The loops below are finite checks (bounded computational validation).

  • Fully general symbolic induction scripts for the underlying recursion laws appear in notebooks/08_induction.ipynb.

Code (finite checks)

is_zero = lambda n: n(lambda _: FALSE)(TRUE)
leq = lambda m: (lambda n: is_zero(sub(m)(n)))

# Associativity test for add
for a in range(4):
    for b in range(4):
        for c in range(4):
            A = from_int(a)
            B = from_int(b)
            C = from_int(c)
            left = add(add(A)(B))(C)
            right = add(A)(add(B)(C))
            assert to_int(left) == to_int(right)

# Distributivity test: a*(b+c) = a*b + a*c
for a in range(4):
    for b in range(4):
        for c in range(4):
            A = from_int(a)
            B = from_int(b)
            C = from_int(c)
            left = mul(A)(add(B)(C))
            right = add(mul(A)(B))(mul(A)(C))
            assert to_int(left) == to_int(right)

Expanded step-by-step example (symbolic rewrite) We document the associativity step for add using the symbolic lambda-calculus engine from Chapter 8.

Goal

\[ \mathrm{add}(\mathrm{add}\;a\;b)(S\,c) = \mathrm{add}\;a\;(\mathrm{add}\;b\;(S\,c)). \]

Step 1 (Definition and Lemma 1)

\[ \mathrm{add}(\mathrm{add}\;a\;b)(S\,c) = S(\mathrm{add}(\mathrm{add}\;a\;b)\;c). \]

Step 2 (Apply IH)

\[ S(\mathrm{add}(\mathrm{add}\;a\;b)\;c) = S(\mathrm{add}\;a\;(\mathrm{add}\;b\;c)). \]

Step 3 (Lemma 1, reversed)

\[ S(\mathrm{add}\;a\;(\mathrm{add}\;b\;c)) = \mathrm{add}\;a\;(\mathrm{add}\;b\;(S\,c)). \]

So the induction step is complete.

Exercise 6 (Recursion and Fixpoints)#

Solution (general proof + code)

Context The fixpoint combinator turns a functional definition into a recursive function. In eager Python, conditionals must be lazy (use IF_THUNK) so the recursive branch is not evaluated when the base case holds.

sum_to_n (formal reasoning) Define primitive recursion:

\[ S(0)=0,\qquad S(n+1)=(n+1)+S(n). \]

This is implemented as:

\[ S = Z\,(\lambda s.\,\lambda n.\,\text{if }n=0\text{ then }0\text{ else }n+s(n-1)). \]

pow (formal reasoning) Define recursion on the exponent:

\[ \mathrm{pow}\;b\;0=1,\qquad \mathrm{pow}\;b\;(S\,e)=b\cdot\mathrm{pow}\;b\;e. \]

fib (formal reasoning)

\[ \mathrm{fib}(0)=0,\quad \mathrm{fib}(1)=1,\quad \mathrm{fib}(n+2)=\mathrm{fib}(n+1)+\mathrm{fib}(n). \]

Church factorial (formal reasoning)

\[ \mathrm{fact}(0)=1,\qquad \mathrm{fact}(S\,n)=(S\,n)\cdot\mathrm{fact}(n). \]

Code

# sum_to_n with eager fixpoint
sum_to_n = Z(lambda s: (lambda n: 0 if n == 0 else n + s(n - 1)))
assert sum_to_n(5) == 15

# Fibonacci on integers (naive recursion, so keep n small)
fib = Z(lambda f: (lambda n: 0 if n == 0 else (1 if n == 1 else f(n - 1) + f(n - 2))))
for n in range(11):
    print(n, fib(n))

# Church pow using recursion and IF_THUNK
IF_THUNK = lambda b: (lambda t: (lambda f: b(t)(f)()))
is_zero = lambda n: n(lambda _: FALSE)(TRUE)

POW = Z(
    lambda pow: (
        lambda b: (
            lambda e: IF_THUNK(is_zero(e))(
                lambda: one
            )(
                lambda: mul(b)(pow(b)(pred(e)))
            )
        )
    )
)
assert to_int(POW(two)(church(5))) == 32

# Church factorial (primitive recursion on n)
FACT = Z(
    lambda fact: (
        lambda n: IF_THUNK(is_zero(n))(
            lambda: one
        )(
            lambda: mul(n)(fact(pred(n)))
        )
    )
)
assert to_int(FACT(church(5))) == 120

Exercise 7 (Peano Arithmetic)#

Solution (general proof + code)

Context We use parity, congruences, and (later) induction objects for general proofs.

Odd/even (formal reasoning) Let \(\mathrm{is\_even}(n) := (n \bmod 2 = 0)\). Define \(\mathrm{is\_odd}(n) := \mathrm{NOT}(\mathrm{is\_even}(n))\).

Commutativity of addition (proof sketch by induction on \(n\))

  1. Base: \(0+m=m=m+0\).

  2. Step: assume \(n+m=m+n\). Then \((S\,n)+m = S(n+m) = S(m+n) = m+(S\,n)\).

(2n)^2 = 4n^2 (algebraic proof) \((2n)^2 = (2n)(2n) = 4n^2\), by associativity and commutativity of multiplication.

Odd square minus one is divisible by 8 (algebraic proof)

\[ (2n+1)^2 - 1 = 4n(n+1). \]

One of \(n\) and \(n+1\) is even, so \(n(n+1)\) is divisible by 2. Hence \(4n(n+1)\) is divisible by 8.

Finite check vs general proof

  • The loops below are finite checks.

  • Fully general induction objects for these statements are in notebooks/09_induction_arithmetic.ipynb.

Code (finite checks)

is_odd = lambda n: NOT(is_even(n))

# Commutativity test for add
for a in range(4):
    for b in range(4):
        A = from_int(a)
        B = from_int(b)
        assert to_int(add(A)(B)) == to_int(add(B)(A))

# Verify (2n)^2 = 4n^2
four = church(4)
for n in range(6):
    N = from_int(n)
    left = mul(mul(two)(N))(mul(two)(N))
    right = mul(four)(mul(N)(N))
    assert to_int(left) == to_int(right)

# Verify (2n+1)^2 - 1 divisible by 8
odd = lambda n: add(mul(two)(n))(one)       # 2n+1
odd_sq_minus_one = lambda n: sub(mul(odd(n))(odd(n)))(one)
div_by_8 = lambda n: is_zero(mod(n)(eight))

for n in range(10):
    N = from_int(n)
    val = odd_sq_minus_one(N)
    assert to_bool(div_by_8(val))

Exercise 8 (Induction)#

Solution (general proofs + code)

8.1 Commutativity of addition

Claim. For all Church numerals \(n,m\), \(\mathrm{add}(n)(m) = \mathrm{add}(m)(n)\).

Induction on \(n\).

  • Base \(n=0\): \(0+m = m = m+0\).

  • Step: assume \(n+m=m+n\). Then \((S\,n)+m = S(n+m) = S(m+n) = m+(S\,n)\), using Lemma 1 and the IH.

Code (finite check)

for a in range(6):
    for b in range(6):
        A = from_int(a)
        B = from_int(b)
        assert to_int(add(A)(B)) == to_int(add(B)(A))

8.2 Associativity of addition

Claim. For all \(n,m,k\),

\[ \mathrm{add}(\mathrm{add}(n)(m))(k) = \mathrm{add}(n)(\mathrm{add}(m)(k)). \]

Induction on \(n\).

  • Base \(n=0\): \((0+m)+k = m+k\) and \(0+(m+k) = m+k\).

  • Step: assume \((n+m)+k = n+(m+k)\). Then

    \[ (S\,n+m)+k = S((n+m)+k) = S(n+(m+k)) = (S\,n)+(m+k). \]

Code (finite check)

for a in range(6):
    for b in range(6):
        for c in range(6):
            A = from_int(a)
            B = from_int(b)
            C = from_int(c)
            lhs = add(add(A)(B))(C)
            rhs = add(A)(add(B)(C))
            assert to_int(lhs) == to_int(rhs)

8.3 A symbolic beta-reduction trace

In Chapter 8 you have symbolic constructors zero_l, succ_l, add_l and helpers succ_term, add_term.

Goal. Reduce \(1+1\) as a symbolic term and inspect the trace.

# Symbolic 1 is succ(zero)
one_l = succ_term(zero_l)
term = add_term(one_l, one_l)
steps = trace_reduce(term, max_steps=12)
for i,s in enumerate(steps):
    print(f"{i:02d}: {show(s)}")
print("NF:", show(nf(term)))

8.4 Multiplication by one

Claim. For all \(n\), \(n\cdot 1 = n\).

Induction on \(n\).

  • Base \(n=0\): \(0\cdot 1 = 0\).

  • Step: using Lemma 2, \((S\,n)\cdot 1 = 1 + (n\cdot 1)\). By IH this is \(1+n = S\,n\).

Code (finite check)

for a in range(8):
    A = from_int(a)
    assert to_int(mul(A)(one)) == a

Exercise 9 (Induction Proofs as Lambda Certificates)#

Solution (explanations + runnable checks)

9.1 Theorem 1: \(2 \mid n(n+1)\)

The chapter defines a certificate generator (witness-producing iterator) for evenness of consecutive products.

for n_int in range(12):
    N = from_int(n_int)
    cert = theorem1_certificate(N)
    assert church_true(cert, f"n={n_int}")

9.2 From Theorem 1 to \((2n+1)^2-1\) divisible by 8

Algebraically:

\[ (2n+1)^2-1 = 4n(n+1). \]

Since \(n(n+1)\) is even, write \(n(n+1)=2r\). Then \((2n+1)^2-1 = 8r\).

In the notebook, this composition is implemented by reusing the witness for Theorem 1 inside the witness for Theorem 4.

for n_int in range(12):
    N = from_int(n_int)
    cert = theorem4_certificate(N)
    assert church_true(cert, f"n={n_int}")

9.3 Sum of odds equals a square

The chapter’s sum_odds_ind is the Church-iterator implementation of

\[ 1+3+\cdots+(2n-1)=n^2. \]
for n_int in range(10):
    N = from_int(n_int)
    lhs = sum_odds_ind(N)
    rhs = mul(N)(N)
    assert to_bool(eq(lhs)(rhs))

Induction step (in words). Assume \(1+3+\cdots+(2n-1)=n^2\). Then adding the next odd number gives \(n^2 + (2n+1) = (n+1)^2\). The certificate iterator encodes exactly this state update.

9.4 Symbolic rewrite trace

Pick one symbolic trace term from Chapter 9 and print the first few beta-reduction steps with trace_reduce, as in Chapter 8.

Exercise 10 (Tooling and Interactivity)#

Solution (how to run the tools)

10.1 Trace table for a simple reduction

expr = r"(\\x. x) (\\y. y)"
display(HTML(format_trace_table(expr, max_steps=6)))

You should see the redex (λx. x) (...) reduce in one step to the identity λy. y.

10.2 Divergence via omega

omega = r"(\\x. x x) (\\x. x x)"
sess = ReductionSession(omega, max_steps=20)
display(sess.widget())
print(sess.render_line())

The session keeps stepping; there is no normal form.

10.3 REPL macros: I, K, and K I omega

repl = LambdaREPL()
print(repl.eval_line(r":let I = \\x. x"))
print(repl.eval_line(r":let K = \\x. \\y. x"))
print(repl.eval_line(r":nf K I ((\\x. x x) (\\x. x x))"))

Under normal order, K I omega reduces to I without evaluating omega.

10.4 Parsing variants of the identity

t1 = parse_lambda("lambda x. x")
t2 = parse_lambda(r"\\x. x")
t3 = parse_lambda("λx. x")
assert pretty(t1) == pretty(t2) == pretty(t3)
print(pretty(t1))

Exercise 11 (Denotational Semantics)#

Solution (math + runnable experiments)

11.1 More Kleene steps for factorial

chain = kleene_chain(FactF, steps=10)
for k, fk in enumerate(chain):
    vals = [fk(n) for n in range(9)]
    print(f"F^{k}(⊥):", vals)

At stage k, factorial values become defined up to roughly k-1.

11.2 A new functional: triangular numbers

Define \(T(0)=0\) and \(T(n+1)=T(n)+(n+1)\).

def TriF(f):
    def g(n):
        if n < 0:
            return BOTTOM
        if n == 0:
            return 0
        prev = f(n - 1)
        if prev is BOTTOM:
            return BOTTOM
        return prev + n
    return g

chain = kleene_chain(TriF, steps=8)
for k, fk in enumerate(chain):
    print(k, [fk(n) for n in range(8)])

11.3 Fuel experiments: terminating vs diverging

print(eval_den(id_term, {}, fuel=10))
print(eval_den(omega, {}, fuel=10))
print(eval_den(omega, {}, fuel=200))

Increasing fuel does not make omega terminate; it just delays when we give up.

11.4 Denotation of (\x. x) (\y. y)

term = App(Lam("x", Var("x")), Lam("y", Var("y")))
print(show(term))
print(eval_den(term, {}, fuel=60))

The denotation is a closure (a semantic function value), not bottom.

Exercise 12 (Normalization by Evaluation)#

Solution (NbE experiments)

12.1 Compose and normalize

A = nbe.Base("A")
X = nbe.Arr(A, A)

id_tm = nbe.TLam(A, nbe.TVar(0))
compose = nbe.TLam(X, nbe.TLam(X, nbe.TLam(A, nbe.TApp(nbe.TVar(2), nbe.TApp(nbe.TVar(1), nbe.TVar(0))))))

term = nbe.TApp(nbe.TApp(compose, id_tm), id_tm)
print(nbe.show_tm(nbe.nbe(term, nbe.Arr(A, A))))

12.2 Eta behavior

eta_candidate = nbe.TLam(A, nbe.TApp(id_tm, nbe.TVar(0)))
print("input:", nbe.show_tm(eta_candidate))
print("nbe  :", nbe.show_tm(nbe.nbe(eta_candidate, nbe.Arr(A, A))))

12.3 Reflect and reify directly

A = nbe.Base("A")
B = nbe.Base("B")
X = nbe.Arr(A, B)

neu = nbe.NVar(0)
val = nbe.reflect(X, neu, lvl=0)
back = nbe.reify(X, val, lvl=0)
print(nbe.show_tm(back))

12.4 Ill-typed application

A = nbe.Base("A")
B = nbe.Base("B")
idA = nbe.TLam(A, nbe.TVar(0))

bad = nbe.TApp(idA, nbe.TLam(B, nbe.TVar(0)))
try:
    nbe.infer(bad, [])
except Exception as e:
    print(type(e).__name__ + ":", e)

The argument has type B -> B, but the function expects A.

Exercise 13 (Dependent Types)#

Solution (Π/Σ inhabitants)

13.1 Dependent identity for EqNat

We want an inhabitant of \(\Pi(n:\mathbb{N}).\mathrm{EqNat}(n)\). The code for this dependent function type is:

Nat = dep.NatCode()
pi_eq = dep.PiCode(Nat, lambda n: dep.EqNatCode(n))

id_nat = lambda n: n
print(dep.is_in(id_nat, pi_eq, depth=6))

13.2 Two witnesses for an existential (Σ-type)

The type \(\Sigma(n:\mathbb{N}).\mathrm{Even}(n)\) is:

Nat = dep.NatCode()
exists_even = dep.SigmaCode(Nat, lambda n: dep.EvenProofCode(n))

w1 = (4, 2)  # 4 = 2*2
w2 = (6, 3)  # 6 = 2*3

print(dep.is_in(w1, exists_even, depth=8))
print(dep.is_in(w2, exists_even, depth=8))

A wrong witness is rejected:

bad = (4, 3)  # claims 4 = 2*3
print(dep.is_in(bad, exists_even, depth=8))

13.3 Implication and conjunction as special cases

A = dep.BoolCode()
B = dep.NatCode()

imp = dep.implies_code(A, B)
conj = dep.and_code(A, B)

f = lambda b: 1 if b else 0
p = (True, 3)

print(dep.is_in(f, imp, depth=6))
print(dep.is_in(p, conj, depth=6))

13.4 A failing dependent proof function

Nat = dep.NatCode()
double_even_type = dep.PiCode(Nat, lambda n: dep.EvenProofCode(2 * n))

good = lambda n: n      # 2n = 2*n
bad = lambda n: n + 1   # claims 2n = 2*(n+1)

print(dep.is_in(good, double_even_type, depth=6))
print(dep.is_in(bad, double_even_type, depth=6))

Exercise 14 (Sequent Calculus)#

Solution (search, extraction, checking)

14.1 K combinator shape: \(A \to (B \to A)\)

A = sq.Atom("A")
B = sq.Atom("B")

goal = sq.Imp(A, sq.Imp(B, A))
_, term = sq.prove_and_extract(goal, depth=12)
print("goal:", sq.show_f(goal))
print("term:", sq.show_t(term))
print("check:", sq.check_nd(term, goal, {}))

14.2 S combinator shape

A = sq.Atom("A")
B = sq.Atom("B")
C = sq.Atom("C")

goal = sq.Imp(
    sq.Imp(A, sq.Imp(B, C)),
    sq.Imp(sq.Imp(A, B), sq.Imp(A, C)),
)
_, term = sq.prove_and_extract(goal, depth=20)
print("goal:", sq.show_f(goal))
print("term:", sq.show_t(term))
print("check:", sq.check_nd(term, goal, {}))

14.3 Peirce’s law fails intuitionistically

A = sq.Atom("A")
B = sq.Atom("B")
peirce = sq.Imp(sq.Imp(sq.Imp(A, B), A), A)

for d in [8, 10, 12, 16, 20]:
    pf = sq.prove((), peirce, depth=d)
    print("depth", d, "->", pf is not None)

14.4 Prove B from context h:A->B and a:A

A = sq.Atom("A")
B = sq.Atom("B")

ctx = (("h", sq.Imp(A, B)), ("a", A))
pf = sq.prove(ctx, B, depth=8)
print("proof exists:", pf is not None)

term = sq.extract_nd(pf)
print("term:", sq.show_t(term))
print("check:", sq.check_nd(term, B, {"h": sq.Imp(A, B), "a": A}))

The extracted term is the expected application h a.

Exercise 15 (Classical Logic via Control)#

Solution (callcc patterns)

15.1 Early exit from nested loops

def first_pair_summing_over(limit=10, threshold=9):
    def body(exit_):
        for x in range(limit):
            for y in range(limit):
                if x + y > threshold:
                    exit_((x, y))
        return None
    return cc.callcc(body)

print(first_pair_summing_over(limit=6, threshold=7))
print(first_pair_summing_over(limit=3, threshold=20))

15.2 first_satisfying

xs = [1, 2, 3, 14, 5]
print(cc.first_satisfying(xs, lambda x: x % 7 == 0, default=None))
ys = [1, 2, 3, 4]
print(cc.first_satisfying(ys, lambda x: x % 7 == 0, default="no hit"))

15.3 Peirce with concrete types

Take A = int, B = str. Provide f : (A->B)->A.

def f(use_ab):
    # We ignore the function and return an A directly.
    return 123

print(cc.peirce(f))

15.4 CPS evaluation with a nontrivial continuation

expr = ("+", 2, ("*", 3, 4))
print(cc.cps_eval(expr, lambda v: v))
print(cc.cps_eval(expr, lambda v: -v))
print(cc.cps_eval(expr, lambda v: v + 1))

The continuation k decides what to do with the final value.

Exercise 16 (System F)#

Solution (polymorphism + checking)

16.1 const term and its type

a = sf.TVar("a")
b = sf.TVar("b")

const_term = sf.TyLam(
    "a",
    sf.TyLam(
        "b",
        sf.Lam("x", a, sf.Lam("y", b, sf.Var("x"))),
    ),
)

print(sf.show_ty(sf.infer(const_term, {}, set())))

Expected type: forall a. forall b. a -> b -> a.

16.2 Instantiate and apply

const_int_bool = sf.TyApp(sf.TyApp(const_term, sf.TInt()), sf.TBool())
full = sf.App(sf.App(const_int_bool, sf.IntLit(7)), sf.BoolLit(True))
print(sf.show_ty(sf.infer(full, {}, set())))

16.3 Incremental instantiation with unification

a = sf.TVar("a")
b = sf.TVar("b")
poly_const = sf.TForall("a", sf.TForall("b", sf.TArrow(a, sf.TArrow(b, a))))

inst = sf.instantiate(poly_const)
print("inst:", sf.show_ty(sf.zonk(inst)))

assert isinstance(inst, sf.TArrow)
sf.unify(inst.left, sf.TInt())
print("after arg Int:", sf.show_ty(sf.zonk(inst.right)))

assert isinstance(inst.right, sf.TArrow)
sf.unify(inst.right.left, sf.TBool())
print("after arg Bool:", sf.show_ty(sf.zonk(inst.right.right)))

16.4 A type error

id_term = sf.TyLam("a", sf.Lam("x", sf.TVar("a"), sf.Var("x")))
id_int = sf.TyApp(id_term, sf.TInt())

bad = sf.App(id_int, sf.BoolLit(True))
try:
    sf.infer(bad, {}, set())
except Exception as e:
    print(type(e).__name__ + ":", e)

The function expects an Int argument but receives a Bool.