๐Ÿฆ€ Functional Rust

142: Type Equality Witnesses

Difficulty: 5 Level: Expert Encode a proof that two types are equal as a Rust value โ€” and use it to safely coerce at compile time.

The Problem This Solves

In type-theoretic programming, you sometimes need to convince the compiler that two type variables are actually the same type โ€” not just compatible, but identical. This arises in generic data structures, type-indexed containers, and translations from languages with GADTs (Generalized Algebraic Data Types). OCaml with GADTs can express this directly: a constructor `Refl : ('a, 'a) eq` is only inhabitable when both type parameters are the same. Rust doesn't have GADTs. But we can simulate the pattern with a `TypeEq<A, B>` struct that can only be constructed when `A = B`. Any function accepting a `TypeEq<A, B>` has a compile-time proof that the types are identical โ€” and can use it to justify coercions that would otherwise be unsound. This is niche, advanced type-level programming, but it's exactly the pattern used in type-safe heterogeneous maps, typed abstract syntax trees, and dependently-typed encodings in Rust.

The Intuition

`TypeEq<A, B>` is a "proof certificate." You can only mint one via `TypeEq::refl()`, which is only callable when `A` and `B` are the same type. Once you have the certificate, you can pass it around and use it as evidence in generic functions that need to know two types are identical. The key properties of type equality hold: reflexivity (`A = A`), symmetry (`A = B โ†’ B = A`), and transitivity (`A = B, B = C โ†’ A = C`). All three are implementable as methods on `TypeEq`, and all three are vacuously sound โ€” they don't change the concrete types, only reflect logical relationships.

How It Works in Rust

use std::marker::PhantomData;

pub struct TypeEq<A, B>(PhantomData<(A, B)>);

impl<T> TypeEq<T, T> {
 // Only constructible when A = B (same type parameter)
 pub fn refl() -> TypeEq<T, T> {
     TypeEq(PhantomData)
 }
}

impl<A, B> TypeEq<A, B> {
 pub fn sym(self) -> TypeEq<B, A> { TypeEq(PhantomData) }

 pub fn trans<C>(self, _: TypeEq<B, C>) -> TypeEq<A, C> {
     TypeEq(PhantomData)
 }
}

// Use the proof in generic code
fn string_length<S: Into<String>>(val: S, _proof: TypeEq<S, String>) -> usize {
 val.into().len()
}

// Only callable with TypeEq<String, String> โ€” proving S *is* String
let proof: TypeEq<String, String> = TypeEq::refl();
println!("{}", string_length("hello".to_string(), proof));
The `PhantomData` carries the type information without any runtime representation. The struct is zero-sized โ€” type equality witnesses have no runtime cost.

What This Unlocks

Key Differences

ConceptOCamlRust
Type equality proof`type ('a, 'b) eq = Refl : ('a, 'a) eq` (GADT)`struct TypeEq<A,B>(PhantomData<(A,B)>)`
Construction`Refl` constructor (GADT constraint)`TypeEq::refl()` (only for `<T,T>`)
Coercion via proof`let Refl = proof in use_as_same_type`Method or `Into` bound
Runtime costZero (erased)Zero (`PhantomData` is zero-sized)
Symmetry`let sym : ('b,'a) eq = Refl``.sym()` method
// Type equality witnesses: a value of type TypeEq<A, B> proves A = B.
//
// In OCaml/Haskell this is used with GADTs to unify type variables.
// Rust doesn't have GADTs, but we can:
//   1. Define TypeEq<A, B> that can only be constructed for TypeEq<T, T>
//   2. Use it as a proof to safely coerce values
//   3. Show symmetry and transitivity
//
// The key insight: TypeEq::refl() is only callable when A = B (same type),
// so any function accepting a TypeEq<A, B> has a compile-time proof A = B.

use std::marker::PhantomData;

// โ”€โ”€ Core TypeEq type โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

/// A witness that types `A` and `B` are equal.
/// Can only be constructed (via `refl`) when `A = B`.
pub struct TypeEq<A, B>(PhantomData<(A, B)>);

impl<T> TypeEq<T, T> {
    /// Construct a proof of `T = T` (reflexivity).
    pub fn refl() -> TypeEq<T, T> {
        TypeEq(PhantomData)
    }
}

impl<A, B> TypeEq<A, B> {
    /// Symmetry: if A = B then B = A.
    pub fn sym(self) -> TypeEq<B, A> {
        TypeEq(PhantomData)
    }

    /// Transitivity: if A = B and B = C then A = C.
    pub fn trans<C>(self, _other: TypeEq<B, C>) -> TypeEq<A, C> {
        TypeEq(PhantomData)
    }

    /// Lift the equality through a container: if A = B then Vec<A> = Vec<B>.
    /// (We can't fully express this in Rust without HKT, but see coerce below.)
    pub fn coerce(self, a: A) -> B
    where
        A: Into<B>,
    {
        a.into()
    }
}

impl<T: Clone> TypeEq<T, T> {
    /// Use the proof to safely "cast" (here: copy) a value.
    pub fn cast(&self, a: T) -> T {
        a
    }
}

// โ”€โ”€ Leibniz-style equality โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// Leibniz: A = B iff for all P, P(A) โ†’ P(B).
// In Rust we approximate this with a concrete container.

pub struct Leibniz<A, B> {
    /// The coercion function: given any P<A>, produce P<B>.
    /// We use Box<A> -> Box<B> as a concrete instantiation.
    coerce: Box<dyn Fn(Box<A>) -> Box<B>>,
}

impl<T: 'static> Leibniz<T, T> {
    pub fn refl() -> Self {
        Leibniz { coerce: Box::new(|x| x) }
    }
}

impl<A: 'static, B: 'static> Leibniz<A, B> {
    pub fn apply(&self, a: A) -> B {
        *((self.coerce)(Box::new(a)))
    }
}

// โ”€โ”€ Type-tagged value (shows coercion in action) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

/// A value tagged with a phantom type.
#[derive(Debug, PartialEq)]
pub struct Tagged<Tag, Val> {
    pub value: Val,
    _tag: PhantomData<Tag>,
}

impl<Tag, Val> Tagged<Tag, Val> {
    pub fn new(value: Val) -> Self {
        Tagged { value, _tag: PhantomData }
    }
}

/// Given TypeEq<A, A>, coerce Tagged<A, V> (trivially).
pub fn coerce_tag<A, V>(
    _eq: TypeEq<A, A>,
    t: Tagged<A, V>,
) -> Tagged<A, V> {
    t // the proof tells us the tags are the same type
}

// โ”€โ”€ Demonstration: use TypeEq in generic code โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

/// A function that, given proof that S = String, turns any S into its length.
fn length_if_string<S>(value: S, _proof: TypeEq<S, String>) -> usize
where
    S: Into<String>,
{
    Into::<String>::into(value).len()
}

fn main() {
    // Reflexivity
    let eq: TypeEq<i32, i32> = TypeEq::refl();
    let n: i32 = 42;
    let m = eq.cast(n);
    assert_eq!(n, m);
    println!("cast_with Refl: {}", m);

    // Symmetry
    let eq2: TypeEq<i32, i32> = TypeEq::refl();
    let _sym: TypeEq<i32, i32> = eq2.sym();
    println!("symmetry holds (TypeEq<i32,i32>.sym() = TypeEq<i32,i32>)");

    // Transitivity
    let eq_ab: TypeEq<i32, i32> = TypeEq::refl();
    let eq_bc: TypeEq<i32, i32> = TypeEq::refl();
    let _eq_ac: TypeEq<i32, i32> = eq_ab.trans(eq_bc);
    println!("transitivity holds");

    // Use proof to coerce String -> String
    let proof: TypeEq<String, String> = TypeEq::refl();
    let len = length_if_string("hello".to_string(), proof);
    println!("length_if_string: {}", len);

    // Tagged coercion
    let tagged = Tagged::<i32, &str>::new("world");
    let eq3: TypeEq<i32, i32> = TypeEq::refl();
    let tagged2 = coerce_tag(eq3, tagged);
    println!("Tagged value: {}", tagged2.value);

    // Leibniz equality
    let leib: Leibniz<String, String> = Leibniz::refl();
    let result = leib.apply("hello".to_string());
    println!("Leibniz coerce: {}", result);

    println!("Type equality proofs work");
}

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

    #[test]
    fn test_refl_cast() {
        let eq: TypeEq<String, String> = TypeEq::refl();
        let s = "hello".to_string();
        assert_eq!(eq.cast(s.clone()), s);
    }

    #[test]
    fn test_sym() {
        let eq: TypeEq<i32, i32> = TypeEq::refl();
        let _sym: TypeEq<i32, i32> = eq.sym();
        // compiles and runs โŸน symmetry works
    }

    #[test]
    fn test_trans() {
        let ab: TypeEq<u8, u8> = TypeEq::refl();
        let bc: TypeEq<u8, u8> = TypeEq::refl();
        let _ac: TypeEq<u8, u8> = ab.trans(bc);
    }

    #[test]
    fn test_length_if_string() {
        let proof: TypeEq<String, String> = TypeEq::refl();
        assert_eq!(length_if_string("rust".to_string(), proof), 4);
    }

    #[test]
    fn test_leibniz() {
        let leib: Leibniz<i32, i32> = Leibniz::refl();
        assert_eq!(leib.apply(42_i32), 42_i32);
    }
}
(* Type equality witnesses: a value of type ('a, 'b) eq proves a = b.
   Useful in GADTs to unify type variables. *)

(* Leibniz equality: a = b iff for all F, F a = F b *)
type ('a, 'b) eq = { cast : 'r. 'r 'a -> 'r 'b }

(* Or simpler with GADTs: *)
type (_, _) teq = Refl : ('a, 'a) teq

(* Using Refl to cast values *)
let cast_with : type a b. (a, b) teq -> a -> b = fun Refl x -> x

(* Symmetry: if a = b then b = a *)
let sym : type a b. (a, b) teq -> (b, a) teq = fun Refl -> Refl

(* Transitivity: if a = b and b = c then a = c *)
let trans : type a b c. (a, b) teq -> (b, c) teq -> (a, c) teq =
  fun Refl Refl -> Refl

let () =
  let eq_int : (int, int) teq = Refl in
  let n : int = 42 in
  let m : int = cast_with eq_int n in
  assert (n = m);
  Printf.printf "cast_with Refl: %d\n" m;

  (* Symmetry *)
  let _sym_eq : (int, int) teq = sym eq_int in
  Printf.printf "Type equality proofs work\n"

๐Ÿ“Š Detailed Comparison

Comparison: Type Equality Constraints

OCaml

๐Ÿช Show OCaml equivalent
type (_, _) eq = Refl : ('a, 'a) eq

let cast : type a b. (a, b) eq -> a -> b = fun Refl x -> x

(* Usage: prove types are equal *)
let add : type a. (a, int) eq -> a -> a -> int =
fun Refl x y -> x + y

Rust

// Associated type equality
fn transfer<A, B>(from: &A, to: &mut B)
where
 A: Container,
 B: Container<Item = A::Item>,  // proves same Item type
{ /* ... */ }

// Witness struct
struct TypeEq<A, B>(PhantomData<(A, B)>);
impl<T> TypeEq<T, T> {
 fn refl() -> Self { TypeEq(PhantomData) }
}