For any formal system, we can really only understand its precise details after attempting to implement it.
-- Simon Thompson, Type Theory & Functional Programming 1
This post has been discussed on Reddit.
This post has a followup.
Since a type theory called "Calculus of Constructions (CoC)" was introduced by Thierry Coquand in 1985, its conceptual elegance has been a subject of interest to many enlightened mathematicians and computer scientists. Since then, its extended variants have found their use in proof assistants, such as Lean and Coq, and programming languages as well, such as Neut, Agda, Idris, and many others. For a logician, CoC can be used as a foundation of (constructive) mathematics; for a programmer, it can be seen as a basis of a programming language whose types correspond to logical propositions, thus making theorem proving and software development proceed within a single system.
The lambda cube is a formal system introduced by Henk Barendregt, that depicts the directions of how simply typed lambda calculus can be extended to form different combinations of terms and types depending on each other (i.e., function types). Thus, vertices in the cube correspond to specific type theories, and the most expressive vertex is CoC itself since it permits all the combinations of terms and types as input and output values:
(Adapted from Wikipedia.)
In the context of programming, this observation sums up the four base paradigms of structuring code:
Combination | Paradigm |
---|---|
Terms depending on terms | Algorithms and data structures. |
Terms depending on types | Generic programming. |
Types depending on types | Type-level programming; metaprogramming. |
Types depending on terms | Dependent typing; formal verification. |
Although CoC is a simple system, it is not straightforward to implement it on a computer: you need to guarantee capture-free substitution, alpha equivalence of terms, uniqueness of types up to beta conversion, and other peculiarities. For this reason, I wrote a minimalistic implementation of CoC in OCaml so that other learners of type theory could benefit from it. You will not see a parser, enhanced diagnostics, or any type system extensions -- everything you will see is just an abstract syntax representation, poor man's type inference, and beta reduction.
As an application of the developed system, I present the encoding of Church booleans, Church numerals, intuitionistic logic, and classical logic with the law of double negation. Finally, I provide unit tests for (almost) all functions involved in the implementation. The characteristics of the implementation are typing à la Church (explicitly typed binders), applicative order evaluation (arguments evaluated first), and a named representation of variables.
All the material presented here should be accessible after reading the first seven chapters from "Type Theory and Formal Proof: An Introduction". Other remarkable literature is included 2 3 4 5.
See also: untyped lambda calculus on the C preprocessor.
Show unit tests
let test_free_vars () =
assert (free_vars (Var "x") = [ "x" ]);
assert (free_vars (lam "x" Star (Var "x")) = []);
assert (free_vars (lam "x" (Var "y") (Var "x")) = [ "y" ]);
assert (free_vars (lam "x" Star (Var "y")) = [ "y" ]);
assert (free_vars (lam "x" (Var "x") (Var "y")) = [ "x"; "y" ]);
assert (free_vars (Appl (Var "x", Var "y")) = [ "x"; "y" ]);
assert (free_vars Star = []);
assert (free_vars Box = [])
let test_binders () =
assert (binders (Var "x") = []);
assert (binders (lam "x" Star (Var "x")) = [ "x" ]);
assert (binders (lam "x" (Var "y") (Var "x")) = [ "x" ]);
assert (binders (lam "x" Star (Var "y")) = [ "x" ]);
assert (binders (lam "x" (pi "y" Star (Var "y")) (Var "x")) = [ "x"; "y" ]);
assert (binders (Appl (Var "x", Var "y")) = []);
assert (
binders (Appl (lam "x" Star (Var "z"), lam "y" Star (Var "z")))
= [ "x"; "y" ]);
assert (binders Star = []);
assert (binders Box = [])
let test_fresh_var () =
assert (freshen [] "x" = "x");
assert (freshen [ "x" ] "y" = "y");
assert (freshen [ "x" ] "x" = "x'");
assert (freshen [ "x"; "x'"; "x''"; "y"; "z" ] "x" = "x'''")
let test_subst () =
assert (subst "x" (Var "y") (Var "x") = Var "y");
assert (subst "y" (Var "z") (Var "x") = Var "x");
assert (subst "x" (Var "z") (Appl (Var "x", Var "y")) = Appl (Var "z", Var "y"));
assert (subst "y" (Var "z") (Appl (Var "x", Var "y")) = Appl (Var "x", Var "z"));
assert (subst "x" (Var "z") (Appl (Var "x", Var "y")) = Appl (Var "z", Var "y"));
assert (subst "z" (Var "l") (Appl (Var "x", Var "y")) = Appl (Var "x", Var "y"));
assert (subst "x" (Var "z") (lam "x" Star (Var "x")) = lam "x" Star (Var "x"));
assert (
subst "x" (Var "z") (lam "x" (Var "x") (Var "x"))
= lam "x" (Var "z") (Var "x"));
assert (subst "y" (Var "z") (lam "x" Star (Var "y")) = lam "x" Star (Var "z"));
(* Check that the new binding variable does not accidentally capture the
substitution term... *)
assert (subst "y" (Var "x") (lam "x" Star (Var "y")) = lam "x'" Star (Var "x"));
(* ... and that it does not capture free variables in the body. *)
assert (
subst "y" (Var "x") (lam "x" Star (Appl (Var "y", Var "x'")))
= lam "x''" Star (Appl (Var "x", Var "x'")));
(* ... and that it does not conflict with bindings in the body. *)
assert (
subst "y" (Var "x") (lam "x" Star (lam "x'" Star (Appl (Var "y", Var "x"))))
= lam "x''" Star (lam "x'" Star (Appl (Var "x", Var "x''"))));
assert (subst "x" (Var "z") Star = Star);
assert (subst "x" (Var "z") Box = Box)
let test_alpha_eq () =
assert (alpha_eq (Var "x", Var "x"));
assert (not (alpha_eq (Var "x", Var "y")));
assert (alpha_eq (Appl (Var "x", Var "y"), Appl (Var "x", Var "y")));
assert (not (alpha_eq (Appl (Var "x", Var "z"), Appl (Var "x", Var "y"))));
assert (not (alpha_eq (Appl (Var "z", Var "y"), Appl (Var "x", Var "y"))));
assert (alpha_eq (lam "x" Star (Var "x"), lam "x" Star (Var "x")));
assert (alpha_eq (lam "x" Star (Var "x"), lam "y" Star (Var "y")));
assert (not (alpha_eq (lam "x" Star (Var "x"), lam "y" (Var "z") (Var "y"))));
assert (alpha_eq (pi "x" Star (Var "x"), pi "x" Star (Var "x")));
assert (alpha_eq (pi "x" Star (Var "x"), pi "y" Star (Var "y")));
assert (not (alpha_eq (pi "x" Star (Var "x"), pi "y" (Var "z") (Var "y"))))
let test_eval () =
assert (eval (Var "x") = Var "x");
assert (eval (Appl (Var "x", Var "y")) = Appl (Var "x", Var "y"));
assert (eval (Appl (lam "x" (Var "a") (Var "x"), Var "y")) = Var "y");
(* If `m` in `Appl(m, n)` is not an immediate lambda, we must first normalise
it and see if it is. *)
assert (
eval
(Appl
( Appl (lam "x" (Var "a") (lam "x" (Var "a") (Var "x")), Var "z"),
Var "y" ))
= Var "y");
(* If `m` in `Appl(m, n)` is not a redex, we must nonetheless evaluate `n` and
return the application. *)
assert (
eval (Appl (Var "x", Appl (lam "x" (Var "a") (Var "x"), Var "y")))
= Appl (Var "x", Var "y"));
(* Check that an argument is evaluated. *)
assert (
eval
(Appl
( lam "x" (Var "a") (Var "x"),
Appl (lam "x" (Var "a") (Var "x"), Var "z") ))
= Var "z");
(* Check that a body after substitution is evaluated. *)
assert (
eval
(Appl
( lam "x" (Var "a") (Appl (lam "y" (Var "a") (Var "y"), Var "x")),
Var "z" ))
= Var "z");
assert (eval (lam "x" (Var "a") (Var "x")) = lam "x" (Var "a") (Var "x"));
(* Both an argument type and a body must be evaluated. *)
assert (
eval
(lam "x"
(Appl (lam "x" (Var "a") (Var "x"), Var "y"))
(Appl (lam "x" (Var "a") (Var "x"), Var "z")))
= lam "x" (Var "y") (Var "z"));
assert (eval Star = Star);
assert (eval Box = Box)
let test_type_inference () =
(* Super-type "*". *)
assert (infer_type Star = Box);
assert (infer_type ~ctx:[ ("x", Star); ("y", Var "x") ] Star = Box);
(* Variables. *)
assert (infer_type ~ctx:[ ("x", Star) ] (Var "x") = Star);
assert (
infer_type ~ctx:[ ("x", Star); ("y", Var "x"); ("z", Var "x") ] (Var "y")
= Var "x");
assert (infer_type ~ctx:[ ("x", Star); ("y", Var "x") ] (Var "y") = Var "x");
(* Application. *)
assert (
infer_type
~ctx:[ ("a", Star); ("y", Var "a") ]
(Appl (lam "x" (Var "a") (Var "x"), Var "y"))
= Var "a");
assert (
infer_type ~ctx:[ ("y", Star) ] (Appl (lam "x" Star (Var "x"), Var "y"))
= Star);
(* A term depending on a term. *)
assert (
infer_type ~ctx:[ ("a", Star) ] (lam "x" (Var "a") (Var "x"))
= pi "x" (Var "a") (Var "a"));
(* A term depending on a type. *)
assert (
infer_type ~ctx:[ ("a", Star); ("y", Var "a") ] (lam "x" Star (Var "y"))
= pi "x" Star (Var "a"));
(* A type depending on a type. *)
assert (infer_type (lam "x" Star (Var "x")) = pi "x" Star Star);
(* A type depending on a term. *)
assert (
infer_type ~ctx:[ ("a", Star) ] (lam "x" (Var "a") (Var "a"))
= pi "x" (Var "a") Star);
assert (
infer_type
~ctx:[ ("a", Star) ]
(lam "x" (Var "a") (pi "y" (Var "a") (Var "a")))
= pi "x" (Var "a") Star);
(* Pi-types. *)
assert (infer_type ~ctx:[ ("a", Star) ] (pi "x" (Var "a") (Var "a")) = Star);
assert (
infer_type
~ctx:[ ("a", Star) ]
(pi "x" (Var "a") (pi "x" (Var "a") (Var "a")))
= Star);
assert (infer_type ~ctx:[ ("a", Box) ] (pi "x" (Var "a") (Var "a")) = Box);
assert (
infer_type
~ctx:[ ("a", Box) ]
(pi "x" (Var "a") (pi "x" (Var "a") (Var "a")))
= Box);
(* Our evaluator must normalise all subexpressions. *)
assert (
infer_type
~ctx:[ ("a", Star); ("b", Star); ("y", Var "a"); ("z", Var "b") ]
(lam "x"
(Appl (lam "x" Star (Var "x"), Var "a"))
(Appl (lam "x" (Var "b") (Var "x"), Var "z")))
= pi "x" (Var "a") (Var "b"))
let test_print () =
assert (print (Var "x") = "x");
assert (print (Appl (Var "x", Var "y")) = "(x y)");
assert (print (lam "x" (Var "z") (Var "x")) = "(λx:z.x)");
assert (print (pi "x" Star (Var "x")) = "(Πx:*.x)");
assert (print (pi "x" (Var "a") (Var "a")) = "(a→a)");
assert (print (pi "a" Star (pi "x" (Var "a") (Var "a"))) = "(Πa:*.(a→a))");
assert (print Star = "*");
assert (print Box = "☐")
let () =
test_free_vars ();
test_binders ();
test_fresh_var ();
test_subst ();
test_alpha_eq ();
test_eval ();
test_type_inference ();
test_print ()