934-church-numerals — Church Numerals
Tutorial
The Problem
Church numerals are a demonstration that numbers and arithmetic can be encoded purely as functions — no integers required. Introduced by Alonzo Church as part of lambda calculus (1930s), a Church numeral N is a function that applies another function N times: zero = λf.λx. x, one = λf.λx. f x, two = λf.λx. f(f x). Successor, addition, and multiplication can all be defined in terms of function composition. This example is primarily educational, illustrating lambda calculus encoding in a typed language. It reveals why Rust requires Rc for shared closures and Box<dyn Fn> for type erasure.
🎯 Learning Outcomes
Box<dyn Fn> for heap-allocated first-class functionsRc to share a closure across multiple calls without CloneCode Example
#![allow(clippy::all)]
/// # Church Numerals — Functions as Numbers
///
/// Lambda calculus encoding of natural numbers using higher-order functions.
/// A Church numeral N is a function that applies `f` N times to `x`.
///
/// In Rust, we use `Box<dyn Fn>` for heap-allocated closures since
/// Rust closures have unique unnameable types (unlike OCaml's uniform representation).
/// Type alias for Church numerals: takes a function and a value, applies f n times.
type Church = Box<dyn Fn(Box<dyn Fn(i64) -> i64>) -> Box<dyn Fn(i64) -> i64>>;
/// Zero: apply f zero times (return x unchanged)
pub fn zero() -> Church {
Box::new(|_f| Box::new(|x| x))
}
/// One: apply f once
pub fn one() -> Church {
Box::new(|f| Box::new(move |x| f(x)))
}
/// Successor: given n, apply f one more time
pub fn succ(n: Church) -> Church {
Box::new(move |f: Box<dyn Fn(i64) -> i64>| {
// We need to share f between n and the extra application
// This is tricky in Rust due to ownership — use Rc
use std::rc::Rc;
let f = Rc::new(f);
let f2 = f.clone();
let inner = n(Box::new(move |x| f2(x)));
let f3 = f.clone();
Box::new(move |x| f3(inner(x)))
})
}
/// Add: m + n = apply f m times, then n times
pub fn add(m: Church, n: Church) -> Church {
Box::new(move |f: Box<dyn Fn(i64) -> i64>| {
use std::rc::Rc;
let f = Rc::new(f);
let f2 = f.clone();
let inner_m = m(Box::new(move |x| f(x)));
let inner_n = n(Box::new(move |x| f2(x)));
Box::new(move |x| inner_m(inner_n(x)))
})
}
/// Convert Church numeral to integer
pub fn to_int(n: Church) -> i64 {
let f: Box<dyn Fn(i64) -> i64> = Box::new(|x| x + 1);
n(f)(0)
}
/// A simpler approach using a concrete recursive type instead of closures
#[derive(Clone, Debug)]
pub enum ChurchNum {
Zero,
Succ(Box<ChurchNum>),
}
impl ChurchNum {
pub fn to_int(&self) -> i64 {
match self {
ChurchNum::Zero => 0,
ChurchNum::Succ(n) => 1 + n.to_int(),
}
}
pub fn from_int(n: i64) -> Self {
if n <= 0 {
ChurchNum::Zero
} else {
ChurchNum::Succ(Box::new(ChurchNum::from_int(n - 1)))
}
}
pub fn add(self, other: Self) -> Self {
match self {
ChurchNum::Zero => other,
ChurchNum::Succ(n) => ChurchNum::Succ(Box::new(n.add(other))),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_zero() {
assert_eq!(to_int(zero()), 0);
}
#[test]
fn test_one() {
assert_eq!(to_int(one()), 1);
}
#[test]
fn test_succ() {
assert_eq!(to_int(succ(zero())), 1);
assert_eq!(to_int(succ(one())), 2);
}
#[test]
fn test_add() {
let two = succ(one());
let three = add(one(), two);
assert_eq!(to_int(three), 3);
}
#[test]
fn test_church_num_adt() {
let three = ChurchNum::from_int(3);
let four = ChurchNum::from_int(4);
assert_eq!(three.add(four).to_int(), 7);
}
#[test]
fn test_church_num_zero() {
assert_eq!(ChurchNum::Zero.to_int(), 0);
}
}Key Differences
Box<dyn Fn> because each closure has a unique type; OCaml's uniform heap representation handles any closure as ('a -> 'b).Rc to share f between multiple applications in succ; OCaml closures are implicitly shared.church = { apply: 'a. ('a -> 'a) -> 'a -> 'a } directly; Rust cannot express rank-2 types without workarounds.OCaml Approach
OCaml's uniform function representation makes Church numerals straightforward: type church = { apply: 'a. ('a -> 'a) -> 'a -> 'a }. let zero = { apply = fun _f x -> x }. let succ n = { apply = fun f x -> f (n.apply f x) }. let to_int n = n.apply (fun x -> x + 1) 0. OCaml's polymorphic 'a. ('a -> 'a) -> 'a -> 'a is a rank-2 polymorphic type — it works for any type 'a. Rust cannot express this without Box<dyn Fn> because closures have unique unnameable types.
Full Source
#![allow(clippy::all)]
/// # Church Numerals — Functions as Numbers
///
/// Lambda calculus encoding of natural numbers using higher-order functions.
/// A Church numeral N is a function that applies `f` N times to `x`.
///
/// In Rust, we use `Box<dyn Fn>` for heap-allocated closures since
/// Rust closures have unique unnameable types (unlike OCaml's uniform representation).
/// Type alias for Church numerals: takes a function and a value, applies f n times.
type Church = Box<dyn Fn(Box<dyn Fn(i64) -> i64>) -> Box<dyn Fn(i64) -> i64>>;
/// Zero: apply f zero times (return x unchanged)
pub fn zero() -> Church {
Box::new(|_f| Box::new(|x| x))
}
/// One: apply f once
pub fn one() -> Church {
Box::new(|f| Box::new(move |x| f(x)))
}
/// Successor: given n, apply f one more time
pub fn succ(n: Church) -> Church {
Box::new(move |f: Box<dyn Fn(i64) -> i64>| {
// We need to share f between n and the extra application
// This is tricky in Rust due to ownership — use Rc
use std::rc::Rc;
let f = Rc::new(f);
let f2 = f.clone();
let inner = n(Box::new(move |x| f2(x)));
let f3 = f.clone();
Box::new(move |x| f3(inner(x)))
})
}
/// Add: m + n = apply f m times, then n times
pub fn add(m: Church, n: Church) -> Church {
Box::new(move |f: Box<dyn Fn(i64) -> i64>| {
use std::rc::Rc;
let f = Rc::new(f);
let f2 = f.clone();
let inner_m = m(Box::new(move |x| f(x)));
let inner_n = n(Box::new(move |x| f2(x)));
Box::new(move |x| inner_m(inner_n(x)))
})
}
/// Convert Church numeral to integer
pub fn to_int(n: Church) -> i64 {
let f: Box<dyn Fn(i64) -> i64> = Box::new(|x| x + 1);
n(f)(0)
}
/// A simpler approach using a concrete recursive type instead of closures
#[derive(Clone, Debug)]
pub enum ChurchNum {
Zero,
Succ(Box<ChurchNum>),
}
impl ChurchNum {
pub fn to_int(&self) -> i64 {
match self {
ChurchNum::Zero => 0,
ChurchNum::Succ(n) => 1 + n.to_int(),
}
}
pub fn from_int(n: i64) -> Self {
if n <= 0 {
ChurchNum::Zero
} else {
ChurchNum::Succ(Box::new(ChurchNum::from_int(n - 1)))
}
}
pub fn add(self, other: Self) -> Self {
match self {
ChurchNum::Zero => other,
ChurchNum::Succ(n) => ChurchNum::Succ(Box::new(n.add(other))),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_zero() {
assert_eq!(to_int(zero()), 0);
}
#[test]
fn test_one() {
assert_eq!(to_int(one()), 1);
}
#[test]
fn test_succ() {
assert_eq!(to_int(succ(zero())), 1);
assert_eq!(to_int(succ(one())), 2);
}
#[test]
fn test_add() {
let two = succ(one());
let three = add(one(), two);
assert_eq!(to_int(three), 3);
}
#[test]
fn test_church_num_adt() {
let three = ChurchNum::from_int(3);
let four = ChurchNum::from_int(4);
assert_eq!(three.add(four).to_int(), 7);
}
#[test]
fn test_church_num_zero() {
assert_eq!(ChurchNum::Zero.to_int(), 0);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_zero() {
assert_eq!(to_int(zero()), 0);
}
#[test]
fn test_one() {
assert_eq!(to_int(one()), 1);
}
#[test]
fn test_succ() {
assert_eq!(to_int(succ(zero())), 1);
assert_eq!(to_int(succ(one())), 2);
}
#[test]
fn test_add() {
let two = succ(one());
let three = add(one(), two);
assert_eq!(to_int(three), 3);
}
#[test]
fn test_church_num_adt() {
let three = ChurchNum::from_int(3);
let four = ChurchNum::from_int(4);
assert_eq!(three.add(four).to_int(), 7);
}
#[test]
fn test_church_num_zero() {
assert_eq!(ChurchNum::Zero.to_int(), 0);
}
}
Deep Comparison
Church Numerals — OCaml vs Rust Comparison
Core Insight
Church numerals are the purest test of first-class function support. OCaml's uniform value representation makes them trivially expressible — let zero _f x = x is all you need. Rust's ownership model creates significant friction: closures capture by move/borrow, have unique types, and can't be easily composed without Box<dyn Fn> and Rc.
OCaml Approach
Elegantly minimal. Functions are first-class values with uniform representation — all the same size on the heap. Composition (mul m n f = m (n f)) is just partial application. Type inference handles everything automatically. This is where ML languages truly shine.
Rust Approach
Two alternatives: (1) Box<dyn Fn> closures with Rc for shared ownership — works but verbose, allocates heavily, and fights the borrow checker. (2) An ADT-based ChurchNum enum (Zero | Succ) — more idiomatic Rust, explicit, and easier to reason about ownership. The ADT approach is essentially Peano numbers.
Comparison Table
| Aspect | OCaml | Rust |
|---|---|---|
| Memory | GC'd closures (uniform) | Box<dyn Fn> + Rc (heap alloc each) |
| Null safety | N/A | N/A |
| Errors | N/A | Ownership errors at compile time |
| Iteration | Partial application | Must clone/Rc-share closures |
| Ergonomics | 1-line definitions | 10+ lines with type annotations |
Things Rust Learners Should Notice
dyn Fn trait objectsRc for shared ownership** — when multiple closures need the same captured valueZero | Succ(Box<N>) avoids closure complexityFurther Reading
Exercises
church_mult(m: Church, n: Church) -> Church that applies m times the application of f n times (composition).church_to_bool using Church booleans: true = λt.λf. t, false = λt.λf. f.church_pred (predecessor function) — note this is significantly harder than successor due to the encoding constraints.