Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Examples

Our Calculus of Constructions implementation demonstrates the full expressiveness of dependent type theory through comprehensive examples that span basic inductive types, higher-order polymorphic functions, universe polymorphism, implicit arguments, and dependent data structures. These examples showcase how the theoretical power of the Calculus of Constructions translates into practical programming language features.

Each example successfully type checks under our implementation, demonstrating the correctness of the constraint solving algorithms, universe system, and dependent type checker. The progression from simple types through complex dependent constructions illustrates how the Calculus of Constructions enables programming patterns while maintaining logical consistency.

Basic Inductive Types and Pattern Matching

The foundation of data structures in the Calculus of Constructions rests on inductive type definitions with constructor-based pattern matching:

#![allow(unused)]
fn main() {
inductive Bool : Type with
| true : Bool
| false : Bool

inductive Nat : Type with
| zero : Nat
| succ : Nat -> Nat

def predecessor (n : Nat) : Nat :=
  match n with
  case zero => zero
  case succ(m) => m

def not_bool (b : Bool) : Bool :=
  match b with
  case true => false
  case false => true
}

These basic examples demonstrate fundamental inductive types including natural numbers and booleans with their associated elimination functions. The predecessor function illustrates how pattern matching provides safe destructuring of inductive values, while the not_bool function shows simple enumeration-style pattern matching. The type checker ensures that all pattern cases are properly handled and that result types are consistent across branches.

Higher-Order Polymorphic Functions

The Calculus of Constructions supports polymorphic programming patterns through its dependent type system:

#![allow(unused)]
fn main() {
inductive Nat : Type with
  | zero : Nat
  | succ : Nat -> Nat

inductive Bool : Type with
  | true : Bool
  | false : Bool

axiom mult : Nat -> Nat -> Nat

def compose (A : Type) (B : Type) (C : Type) (g : B -> C) (f : A -> B) (x : A) : C :=
  g (f x)

def square (x : Nat) : Nat :=
  mult x x

def doTwice (A : Type) (h : A -> A) (x : A) : A :=
  h (h x)

def doThrice (A : Type) (h : A -> A) (x : A) : A :=
  h (h (h x))
}

These examples showcase the interaction between inductive types, higher-order functions, and polymorphic composition. The compose function demonstrates parametric polymorphism over three type parameters, while doTwice and doThrice show how higher-order functions can abstract over computational patterns. The square function illustrates interaction with primitive operations, showing how user-defined types integrate with built-in arithmetic.

Universe Polymorphism and Level Abstraction

Universe polymorphism enables definitions that work across the entire universe hierarchy, providing genuine genericity over type levels:

#![allow(unused)]
fn main() {
-- Universe Polymorphism Examples
-- Demonstrates universe-polymorphic definitions in CoC

-- 1. Basic universe polymorphic identity
def id.{u} (A : Sort (u+1)) (x : A) : A := x

-- 2. Universe polymorphic constant function  
def const.{u, v} (A : Sort (u+1)) (B : Sort (v+1)) (x : A) (y : B) : A := x

-- 3. Universe polymorphic composition
def compose.{u, v, w} (A : Sort (u+1)) (B : Sort (v+1)) (C : Sort (w+1)) (g : B -> C) (f : A -> B) (x : A) : C := g (f x)

-- 4. Tests at different universe levels
-- At Type level (u = Zero)
axiom test_id_type : (A : Type) -> A -> A
def id_at_type : (A : Type) -> A -> A := id

-- At Prop level (u = -1, but we use Zero for Prop)  
axiom test_id_prop : (A : Prop) -> A -> A

-- 5. Inductive types with universe parameters
inductive List.{u} (A : Sort (u+1)) : Sort (u+1) with
  | nil : List A
  | cons : A -> List A -> List A

-- 6. Higher-order universe polymorphism
def map.{u, v} (A : Sort (u+1)) (B : Sort (v+1)) (f : A -> B) (l : List A) : List B := match l with case nil => nil B case cons(x, xs) => cons B (f x) (map A B f xs)

-- 7. Universe constraints with max
axiom pair_type.{u, v} : Sort (u+1) -> Sort (v+1) -> Sort (u+1)

-- 8. Dependent types with universe parameters
def dependent_id.{u} (A : Sort (u+1)) (P : A -> Sort (u+1)) (x : A) (px : P x) : P x := px

-- 9. Examples of universe level arithmetic
-- Type 0 = Sort 1
-- Type 1 = Sort 2  
-- Type n = Sort (n+1)
axiom level0 : Type       -- Type 0 = Sort 1
axiom level1 : Type 1     -- Type 1 = Sort 2
axiom level2 : Type 2     -- Type 2 = Sort 3

-- 10. Universe polymorphic structures
structure Pair.{u, v} (A : Sort (u+1)) (B : Sort (v+1)) : Sort (u+1) :=
  (fst : A)
  (snd : B)
}

These examples demonstrate universe-polymorphic definitions that abstract over universe levels using explicit level parameters. The id.{u} function works at any universe level, while const.{u,v} shows polymorphism over multiple universe parameters. The inductive type List.{u} demonstrates universe-polymorphic data structures that can contain elements at arbitrary universe levels.

The universe arithmetic expressions like Sort (u+1) show how the universe solver handles level arithmetic during type checking. Universe constraints ensure that polymorphic instantiations respect the universe hierarchy while enabling maximum flexibility in generic programming.

Comprehensive Implicit Arguments

Implicit arguments provide syntactic convenience while maintaining the full expressiveness of dependent types:

#![allow(unused)]
fn main() {
-- Comprehensive tests for implicit arguments with constraint solving

-- Basic datatypes
inductive Nat : Type with
  | zero : Nat
  | succ : Nat -> Nat

inductive List (A : Type) : Type with
  | nil : List A
  | cons : A -> List A -> List A

inductive Pair (A : Type) (B : Type) : Type with
  | mkPair : A -> B -> Pair A B

-- Test 1: Simple implicit argument
def id {A : Type} (x : A) : A := x

-- Test 2: Multiple implicit arguments  
def const {A : Type} {B : Type} (x : A) (y : B) : A := x

-- Test 3: Implicit with explicit type application
def apply_id : Nat -> Nat := id

-- Test 4: Multiple implicit arguments with inference
def first : Nat -> Nat -> Nat := const

-- Test 5: Nested implicit arguments
def compose {A : Type} {B : Type} {C : Type} 
           (g : B -> C) (f : A -> B) (x : A) : C :=
  g (f x)

-- Test 6: Implicit arguments in inductive type constructors
def empty_list : List Nat := nil Nat

def nat_list : List Nat := cons Nat zero (nil Nat)

-- Test 7: Implicit with dependent types
def replicate {A : Type} (n : Nat) (x : A) : List A :=
  match n with
  case zero => nil A
  case succ(m) => cons A x (replicate m x)

-- Test 8: Higher-order functions with implicits
def map {A : Type} {B : Type} (f : A -> B) (l : List A) : List B :=
  match l with
  case nil => nil B
  case cons(x, xs) => cons B (f x) (map f xs)

-- Test 9: Polymorphic pair operations
def fst {A : Type} {B : Type} (p : Pair A B) : A :=
  match p with
  case mkPair(a, b) => a

def snd {A : Type} {B : Type} (p : Pair A B) : B :=
  match p with
  case mkPair(a, b) => b

-- Test 10: Complex composition with implicits
def double_map {A : Type} {B : Type} {C : Type}
               (f : B -> C) (g : A -> B) (l : List A) : List C :=
  map f (map g l)

-- Test 11: Implicit arguments in let bindings
def test_let : Nat :=
  let f := id in
  f zero

-- Test 12: Implicit arguments with universe polymorphism
-- axiom Type_id {u : Level} : Type u -> Type u
-- def type_identity : Type -> Type := Type_id

-- Test cases that should work
def test_id_nat : Nat := id zero
def test_const_nat : Nat := const zero (succ zero)
def test_compose_nat : Nat -> Nat := compose succ succ
def test_pair : Pair Nat Nat := mkPair Nat Nat zero (succ zero)
def test_fst : Nat := fst (mkPair Nat Nat zero (succ zero))

-- Test that implicit arguments are correctly inferred
def complex_test : List Nat :=
  map succ (cons Nat zero (cons Nat (succ zero) (nil Nat)))
}

This comprehensive example demonstrates the full range of implicit argument features. Simple functions like id {A : Type} show basic type inference, while complex functions like double_map demonstrate implicit argument propagation through nested polymorphic calls. The constraint solver automatically infers type arguments based on usage context, eliminating boilerplate while maintaining type safety.

The map and replicate functions show how implicit arguments work with recursive functions and pattern matching. The constraint solver tracks type relationships across recursive calls, ensuring that implicit arguments are consistently instantiated throughout the computation.

Dependent Data Structures

Dependent types enable data structures whose types encode computational properties, providing compile-time guarantees about program behavior:

#![allow(unused)]
fn main() {
inductive Nat : Type with
  | zero : Nat
  | succ : Nat -> Nat

inductive Vec (A : Type) : Nat -> Type with  
  | nil : Vec A zero
  | cons : (n : Nat) -> A -> Vec A n -> Vec A (succ n)
}

Length-indexed vectors represent the paradigmatic example of dependent data types. The Vec A n type carries its length n at the type level, enabling operations that are guaranteed to respect vector bounds at compile time. The nil constructor produces vectors of length zero, while cons extends vectors by exactly one element.

This example demonstrates how dependent types can encode invariants that would traditionally require runtime checking. The type system ensures that vector operations respect length constraints, eliminating entire classes of array bounds errors at compile time.

Structure Types and Record Operations

Structure types provide named field access and demonstrate the Calculus of Constructions’ support for record-like data organization:

#![allow(unused)]
fn main() {
inductive Nat : Type with
| zero : Nat
| succ : Nat -> Nat

structure Point : Type := (x : Nat) (y : Nat)

axiom point_instance : Point

axiom Point_x : Point -> Nat
axiom Point_y : Point -> Nat

def get_x_coordinate : Nat := Point_x point_instance
}

The Point structure demonstrates basic record types with named fields. While full dot notation requires additional parser support, the example shows how structures integrate with the dependent type system. Structure types can participate in dependent constructions, enabling data modeling patterns.

Polymorphic Function Composition

Complex polymorphic programming demonstrates the interaction between higher-order functions, type inference, and constraint solving:

#![allow(unused)]
fn main() {
def compose (A : Type) (B : Type) (C : Type) : (B -> C) -> (A -> B) -> A -> C :=
  fun g => fun f => fun x => g (f x)

def doTwice (A : Type) : (A -> A) -> A -> A :=
  fun h => fun x => h (h x)
}

These examples show curried function definitions that demonstrate the lambda calculus foundations of the Calculus of Constructions. The compose function shows three-way type polymorphism, while doTwice demonstrates how polymorphic higher-order functions can abstract over computational patterns.

Working Examples with Type Inference

Practical examples demonstrate how the constraint solver handles complex type inference scenarios:

#![allow(unused)]
fn main() {
axiom id_type : Type -> Type -> Type

def simple_function : Type -> Type := fun A => A

inductive MyBool : Type with
  | true : MyBool
  | false : MyBool

def constFunc (A : Type) (B : Type) : A -> B -> A :=
  fun x => fun y => x
}

These working examples demonstrate the type inference engine in action. The constFunc definition shows how the constraint solver handles nested lambda abstractions with polymorphic types. The simple_function illustrates type-level computation, where functions can manipulate types as first-class values.

Advanced Dependent Programming

The most examples demonstrate the full power of dependent types in practical programming scenarios:

#![allow(unused)]
fn main() {
-- Dependent types example

def id (A : Type) (x : A) : A := x

axiom Nat : Type
def test_id_nat : Nat -> Nat := id Nat
}

These examples show how dependent types enable programs where types can depend on term values. The id function with explicit type parameters demonstrates how dependent functions can be polymorphic over both types and their computational properties.