๐Ÿฆ€ Functional Rust

233: Curry-Howard Correspondence

Difficulty: โญโญโญ Level: Category Theory Types are propositions. Programs are proofs. A function `fn(A) -> B` is a proof that "if A is true, then B is true."

The Problem This Solves

Why does a type-safe Rust program have no undefined behavior? Why does the compiler guarantee memory safety? The answer isn't just "the borrow checker" โ€” it's something deeper: the type system is a logic, and every well-typed program is a proof in that logic. The Curry-Howard correspondence is the formal statement of this connection. Once you see it, the design of Rust's type system stops feeling arbitrary. `Option<T>` exists because "T might not be present" is a logical proposition with a precise type-level encoding. `Never` (`!`) exists because some computations truly never return, and that's a logical fact that deserves a type. `fn(A, B) -> C` exists because it's the type of proofs of "A AND B implies C." This correspondence also explains why dependent types and proof assistants (like Coq, Lean, Agda) look so much like programming languages โ€” they literally are.

The Intuition

Every proposition in logic corresponds to a type. Every proof of a proposition corresponds to a value of that type.
LogicRust
Proposition `A`Type `A`
Proof of `A`Value of type `A`
`A AND B``(A, B)` โ€” must have both
`A OR B``enum Either { Left(A), Right(B) }`
`A IMPLIES B``fn(A) -> B` โ€” given a proof of A, produce a proof of B
`TRUE``()` โ€” trivially inhabited, trivially provable
`FALSE``!` (Never) โ€” uninhabited, unprovable
`NOT A``fn(A) -> !` โ€” if A were true, you'd have a contradiction
A type is "inhabited" if it has at least one value. In logic: a proposition is "provable" if it has at least one proof. An inhabited type = a provable proposition. To prove `A AND B`, you must produce both an `A` and a `B`. That's why conjunction is a product type. To prove `A OR B`, you need only one: either an `A` or a `B`. That's why disjunction is a sum type. To prove `A IMPLIES B`, you need a function: given any proof of `A`, produce a proof of `B`. That's why implication is a function type.

How It Works in Rust

// AND: proof requires both components
struct And<A, B>(A, B);

fn and_intro<A, B>(a: A, b: B) -> And<A, B> { And(a, b) }       // prove AโˆงB from A, B
fn and_elim_left<A: Clone, B>(p: &And<A, B>) -> A { p.0.clone() } // extract proof of A
fn and_elim_right<A, B: Clone>(p: &And<A, B>) -> B { p.1.clone() }

// OR: proof requires one component
enum Or<A, B> { Left(A), Right(B) }

fn or_intro_left<A, B>(a: A) -> Or<A, B>  { Or::Left(a) }  // prove AโˆจB from just A
fn or_intro_right<A, B>(b: B) -> Or<A, B> { Or::Right(b) }

// Elimination: given AโˆจB, plus f:A->C and g:B->C, prove C
fn or_elim<A, B, C>(or: Or<A, B>, f: impl Fn(A)->C, g: impl Fn(B)->C) -> C {
 match or { Or::Left(a) => f(a), Or::Right(b) => g(b) }
}

// IMPLICATION: a function from proof of A to proof of B
fn modus_ponens<A, B>(f: impl Fn(A) -> B, a: A) -> B { f(a) }

// Actual theorems as functions:
fn and_comm<A: Clone, B: Clone>(proof: And<A, B>) -> And<B, A> {
 And(proof.1, proof.0)  // proof that AโˆงB โ†’ BโˆงA
}
fn identity_proof<A>(a: A) -> A { a }  // proof that A โ†’ A (tautology)
The type `fn(A) -> B` is only inhabited if B is derivable from A. A `fn(i32) -> String` exists; a `fn(String) -> i32` also exists (parsing may fail, hence `Result`). But a `fn(()) -> !` cannot be written (there's no value of type `!` to return).

What This Unlocks

Key Differences

ConceptOCamlRust
ConjunctionTuple `(a * b)`Struct or tuple `(A, B)`
DisjunctionSum type `a \b``enum Or<A, B>` / `Result<A, B>`
ImplicationFunction `a -> b``fn(A) -> B`
False / BottomPolymorphic `'a` (empty type)`!` / `std::convert::Infallible`
Negation`'a -> 'b` (bottom encoding)`fn(A) -> !` (function to Never)
/// Curry-Howard correspondence: propositions are types, proofs are programs.
///
/// - A -> B         corresponds to a function `fn(A) -> B`
/// - A /\ B (And)   corresponds to a pair `(A, B)`
/// - A \/ B (Or)    corresponds to `Result<A, B>` or an enum
/// - True           corresponds to `()` (unit โ€” trivially inhabited)
/// - False          corresponds to `!` (never โ€” uninhabited)
/// - Not A          corresponds to `fn(A) -> !` (a function to Never)
///
/// A "proof" of a proposition is a value of the corresponding type.
/// If a type is inhabited (has a value), the proposition is provable.

// โ”€โ”€ Conjunction (A โˆง B) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

/// Proof of "A and B": we must provide both A and B.
struct And<A, B>(A, B);

fn and_intro<A, B>(a: A, b: B) -> And<A, B> {
    And(a, b)
}

fn and_elim_left<A: Clone, B>(proof: &And<A, B>) -> A {
    proof.0.clone()
}

fn and_elim_right<A, B: Clone>(proof: &And<A, B>) -> B {
    proof.1.clone()
}

// โ”€โ”€ Disjunction (A โˆจ B) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

/// Proof of "A or B": we provide either A or B (but not necessarily both).
/// In Rust, `Result<A, B>` (or any enum) serves this role.
/// We use our own type for clarity.
enum Or<A, B> {
    Left(A),
    Right(B),
}

fn or_intro_left<A, B>(a: A) -> Or<A, B> {
    Or::Left(a)
}

fn or_intro_right<A, B>(b: B) -> Or<A, B> {
    Or::Right(b)
}

fn or_elim<A, B, C>(or: Or<A, B>, f: impl Fn(A) -> C, g: impl Fn(B) -> C) -> C {
    match or {
        Or::Left(a) => f(a),
        Or::Right(b) => g(b),
    }
}

// โ”€โ”€ Implication (A โ†’ B) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

/// Modus ponens: given a proof of A->B and a proof of A, produce proof of B.
fn modus_ponens<A, B>(f: impl Fn(A) -> B, a: A) -> B {
    f(a)
}

// โ”€โ”€ Negation (ยฌA = A โ†’ โŠฅ) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

/// Negation as a function to the never type.
/// In stable Rust, `!` (Never) cannot be used in all positions yet,
/// so we use a phantom approach. In nightly `!` is fully usable.
///
/// We model ยฌA as: a proof that A leads to contradiction.
/// Since we can't easily use `!` as a return type in stable Rust for trait objects,
/// we demonstrate the concept with a marker.
struct Not<A>(std::marker::PhantomData<fn(A) -> std::convert::Infallible>);

/// Double negation introduction: A โŠข ยฌยฌA
/// Given a proof of A, we can always produce a proof of ยฌยฌA.
/// (The converse โ€” double-negation elimination โ€” requires classical logic.)
fn double_neg_intro<A>(_proof_of_a: &A) -> Box<dyn Fn(&dyn Fn(&A)) -> ()> {
    // ยฌยฌA = (A -> False) -> False
    // We return a function that takes (ยฌA) and applies it to our A.
    // In constructive logic, this is valid: given A, give me ยฌA and I'll derive โŠฅ.
    Box::new(|_neg_a: &dyn Fn(&A)| {
        // neg_a(_proof_of_a) would be โŠฅ, but we can't fully express this without !
        // The type-level encoding shows the structure is correct.
    })
}

// โ”€โ”€ Some actual proofs โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

/// Proof: A โˆง B โ†’ B โˆง A  (conjunction is commutative)
fn and_comm<A: Clone, B: Clone>(proof: And<A, B>) -> And<B, A> {
    And(proof.1, proof.0)
}

/// Proof: A โ†’ A  (identity / reflexivity)
fn identity_proof<A>(a: A) -> A {
    a
}

/// Proof: A โˆง (A โ†’ B) โ†’ B  (modus ponens as a conjunction)
fn and_modus_ponens<A: Clone, B>(proof: And<A, impl Fn(A) -> B>) -> B {
    (proof.1)(proof.0)
}

/// Proof: (A โˆจ B) โˆง ยฌA โ†’ B
/// If we have "A or B" and "not A", then we must have B.
fn disjunctive_syllogism<A, B>(
    or_proof: Or<A, B>,
    _refutation: impl Fn(A) -> B,  // ยฌA = A -> contradiction-ish
) -> B {
    match or_proof {
        Or::Left(a) => _refutation(a),
        Or::Right(b) => b,
    }
}

fn main() {
    println!("=== Curry-Howard Correspondence ===\n");
    println!("Propositions are types. Proofs are programs.\n");

    // Proof of "Int AND String"
    let conj = and_intro(42_i32, "hello");
    let left = and_elim_left(&conj);
    let right = and_elim_right(&conj);
    println!("And(42, \"hello\"): left={}, right={}", left, right);

    // Commutativity of And
    let swapped = and_comm(and_intro(1_i32, "world"));
    println!("and_comm: ({}, {})", swapped.0, swapped.1);

    // Proof of "Int OR String" via left injection
    let disj: Or<i32, &str> = or_intro_left(42);
    let result = or_elim(disj, |n| format!("number: {}", n), |s| format!("string: {}", s));
    println!("Or::Left(42): {}", result);

    // Modus ponens: we have proof of A and proof of A->B, so B
    let mp_result = modus_ponens(|n: i32| n * 2, 21);
    println!("Modus ponens (21 * 2 = {})", mp_result);

    // Identity: proof that A -> A always holds
    let id_result = identity_proof(99_i32);
    println!("Identity proof: {}", id_result);

    println!();
    println!("Key insight: if a function type is inhabited (has an implementation),");
    println!("the corresponding logical implication is provable.");
    println!("fn(A) -> B  โ‰ก  A implies B");
    println!("fn(A) -> !  โ‰ก  A is false (A implies absurdity)");
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_and_intro_elim() {
        let proof = and_intro(10_i32, "ten");
        assert_eq!(and_elim_left(&proof), 10);
        assert_eq!(and_elim_right(&proof), "ten");
    }

    #[test]
    fn test_and_comm() {
        let proof = and_intro(1_i32, 2_i32);
        let swapped = and_comm(proof);
        assert_eq!(swapped.0, 2);
        assert_eq!(swapped.1, 1);
    }

    #[test]
    fn test_or_elim() {
        let left: Or<i32, &str> = or_intro_left(42);
        let r = or_elim(left, |n| n * 2, |_| 0);
        assert_eq!(r, 84);

        let right: Or<i32, i32> = or_intro_right(7);
        let r2 = or_elim(right, |n| n + 100, |n| n + 1);
        assert_eq!(r2, 8);
    }

    #[test]
    fn test_modus_ponens() {
        let result = modus_ponens(|s: &str| s.len(), "hello");
        assert_eq!(result, 5);
    }

    #[test]
    fn test_identity() {
        assert_eq!(identity_proof(42_i32), 42);
        assert_eq!(identity_proof("hello"), "hello");
    }
}
(* Curry-Howard: propositions are types, proofs are programs.
   A -> B corresponds to a function a -> b.
   A /\ B corresponds to a pair (a, b).
   A \/ B corresponds to a sum type (Left a | Right b). *)

(* Conjunction = product *)
type ('a, 'b) conj = And of 'a * 'b

let and_intro a b = And (a, b)
let and_elim_left  (And (a, _)) = a
let and_elim_right (And (_, b)) = b

(* Disjunction = sum *)
type ('a, 'b) disj = Left of 'a | Right of 'b

let or_intro_left  a = Left a
let or_intro_right b = Right b
let or_elim f g = function Left a -> f a | Right b -> g b

(* Implication = function *)
let modus_ponens f a = f a  (* A -> B, A |- B *)

(* Negation = A -> False (using unit as False placeholder) *)
type 'a neg = 'a -> unit

(* Double negation intro *)
let dne : 'a -> 'a neg neg = fun a k -> k a

let () =
  (* Proof: A /\ B -> B /\ A (and is commutative) *)
  let and_comm (And (a, b)) = And (b, a) in
  let proof = and_comm (And (1, "hello")) in
  Printf.printf "and_comm: (%s, %d)\n" (and_elim_left proof) (and_elim_right proof);

  (* Proof: A -> A (identity) *)
  let id_proof : 'a -> 'a = modus_ponens (fun x -> x) in
  Printf.printf "identity proof: %d\n" (id_proof 42);

  Printf.printf "Curry-Howard: types are propositions, functions are proofs\n"