742: Type Witnesses and Evidence Passing
Difficulty: 4 Level: Expert A type witness is a value whose type proves an invariant holds โ `Sorted<T>` guarantees the contents are sorted, `NonZeroU64` guarantees non-zero โ so operations that require the invariant can accept the witness instead of re-checking.The Problem This Solves
Binary search requires a sorted slice. Every time you call binary search, someone has to ensure the slice is sorted โ either you check it at the call site (expensive and easy to forget), you add an assertion inside the function (runtime cost on every call, checked once when invariant was established), or you trust the caller (unsafe contract, violated eventually). The same pattern recurs constantly: divide-by-zero requires a non-zero divisor, authenticated endpoints require an authenticated session, safe indexing requires an in-bounds index. These invariants get re-checked redundantly or, worse, get asserted at the wrong level. Type witnesses encode the proof in the type. `Sorted<T>` is a wrapper around `Vec<T>` that can only be created by actually sorting. Once you hold a `Sorted<T>`, the sorting invariant is established and any function that requires sorted input accepts `&Sorted<T>` โ no re-check needed. The compiler enforces that you can't construct `Sorted<T>` any other way.The Intuition
A witness is a value you can only obtain by satisfying a condition. The private inner field is the enforcement mechanism โ `Sorted<T>(Vec<T>)` with a private constructor means the only way to get a `Sorted<T>` is through `Sorted::sort(v)`. Functions like `merge()` return `Sorted<T>` because merging two sorted sequences produces a sorted sequence โ the type propagates the proof. This is proof-carrying code in Rust. You don't pass "is this sorted?" as a boolean โ you pass the sorting proof itself as the type. A function that requires a sorted sequence makes the requirement visible in its signature: `fn binary_search(sorted: &Sorted<T>, target: &T)`.How It Works in Rust
// โโ Sorted witness โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
/// Private inner field prevents construction outside the module.
/// Only `sort()` can produce a `Sorted<T>`.
pub struct Sorted<T>(Vec<T>); // private field โ can't construct directly
impl<T: Ord + Clone> Sorted<T> {
/// Only entry point โ sorting produces the witness
pub fn sort(mut v: Vec<T>) -> Self {
v.sort();
Sorted(v) // now we KNOW it's sorted
}
/// Merging two sorted sequences produces a sorted sequence โ type propagates
pub fn merge(self, other: Sorted<T>) -> Sorted<T> {
// merge-sort merge step โ result is guaranteed sorted
Sorted(/* merge impl */)
}
/// Binary search โ safe to call because we KNOW the invariant holds
pub fn binary_search(&self, target: &T) -> bool {
self.0.binary_search(target).is_ok()
// No pre-check needed โ Sorted<T> IS the proof it's sorted
}
}
// โโ NonZero witness โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
pub struct NonZeroU64(u64); // private field
impl NonZeroU64 {
pub fn new(n: u64) -> Option<Self> {
if n == 0 { None } else { Some(NonZeroU64(n)) }
}
/// Division never panics โ the non-zero invariant is in the type
pub fn divide(self, dividend: u64) -> u64 {
dividend / self.0 // SAFE: self.0 โ 0 by construction
}
}
// โโ Authentication witness โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
use std::marker::PhantomData;
pub struct Authenticated;
pub struct Unauthenticated;
pub struct Session<Auth> { user_id: u64, _auth: PhantomData<Auth> }
impl Session<Unauthenticated> {
pub fn new() -> Self { Session { user_id: 0, _auth: PhantomData } }
pub fn authenticate(self, user_id: u64, _password: &str) -> Session<Authenticated> {
// Verification happens HERE โ the Authenticated witness is the proof
Session { user_id, _auth: PhantomData }
}
}
impl Session<Authenticated> {
// This fn only accepts authenticated sessions โ no runtime auth check needed
pub fn access_profile(&self) -> String { format!("Profile for {}", self.user_id) }
}
// Usage:
let data = vec![5, 2, 8, 1, 9, 3];
let sorted = Sorted::sort(data);
println!("{}", sorted.binary_search(&8)); // true โ no re-sort needed
let divisor = NonZeroU64::new(5).unwrap();
println!("{}", divisor.divide(100)); // 20 โ no division-by-zero check
// session.access_profile(); // COMPILE ERROR โ Unauthenticated
let auth = Session::<Unauthenticated>::new().authenticate(42, "secret");
println!("{}", auth.access_profile()); // authenticated โ safe
What This Unlocks
- Eliminate redundant checks โ establish the invariant once (at construction), carry the proof in the type, never re-check inside functions that receive the witness.
- Self-documenting preconditions โ `fn binary_search(sorted: &Sorted<T>)` communicates the precondition in the type; no doc comment needed.
- Correct-by-construction APIs โ merge sort, authenticated routes, divisor arguments โ each witness carries its proof transitively through operations that preserve the invariant.
Key Differences
| Concept | OCaml | Rust |
|---|---|---|
| Sorted witness | `module SortedList : sig type 'a t; val sort: ... end` โ private constructor via signature | `Sorted<T>(Vec<T>)` with private field โ only `sort()` constructs it |
| Proof-carrying types | GADT witnesses: `type _ is_sorted = IsSorted : sorted is_sorted` | Phantom type parameter or private-field newtype |
| Non-zero witness | `type nonzero = private int` โ private in signature | `NonZeroU64(u64)` with private field; `new()` returns `Option` |
| Authentication witness | Abstract type in module signature | `Session<Authenticated>` vs `Session<Unauthenticated>` phantom types |
| Performance | Zero cost (GC; no runtime check after proof) | Zero cost โ type erased; no runtime representation of the witness |
/// 742: Type Witnesses โ evidence passing for invariants
// โโ Sorted witness โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
/// A `Vec<T>` that is guaranteed to be sorted.
/// The private inner field prevents construction without going through `sort()`.
#[derive(Debug, Clone)]
pub struct Sorted<T>(Vec<T>);
impl<T: Ord + Clone> Sorted<T> {
/// Only entry point โ the act of sorting produces the witness.
pub fn sort(mut v: Vec<T>) -> Self {
v.sort();
Sorted(v)
}
/// Merge two sorted vecs โ result is still sorted (merge sort merge step).
pub fn merge(self, other: Sorted<T>) -> Sorted<T> {
let mut a = self.0.into_iter().peekable();
let mut b = other.0.into_iter().peekable();
let mut result = Vec::with_capacity(a.size_hint().0 + b.size_hint().0);
loop {
match (a.peek(), b.peek()) {
(Some(av), Some(bv)) => {
if av <= bv { result.push(a.next().unwrap()); }
else { result.push(b.next().unwrap()); }
}
(Some(_), None) => { result.extend(a); break; }
(None, Some(_)) => { result.extend(b); break; }
(None, None) => break,
}
}
Sorted(result) // safe: merge of two sorted = sorted
}
/// Binary search โ safe to use because we KNOW it's sorted.
pub fn binary_search(&self, target: &T) -> bool {
self.0.binary_search(target).is_ok()
}
pub fn as_slice(&self) -> &[T] { &self.0 }
pub fn len(&self) -> usize { self.0.len() }
pub fn is_empty(&self) -> bool { self.0.is_empty() }
}
// โโ NonZero witness โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
/// A `u64` guaranteed to be non-zero.
#[derive(Debug, Clone, Copy)]
pub struct NonZeroU64(u64);
impl NonZeroU64 {
pub fn new(n: u64) -> Option<Self> {
if n == 0 { None } else { Some(NonZeroU64(n)) }
}
pub fn get(self) -> u64 { self.0 }
/// Division by non-zero โ never panics (no division-by-zero possible).
pub fn divide(self, dividend: u64) -> u64 {
dividend / self.0 // safe: self.0 guaranteed โ 0
}
}
// โโ Authenticated witness โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
pub struct Authenticated;
pub struct Unauthenticated;
use std::marker::PhantomData;
pub struct Session<Auth> {
user_id: u64,
_auth: PhantomData<Auth>,
}
impl Session<Unauthenticated> {
pub fn new() -> Self { Session { user_id: 0, _auth: PhantomData } }
pub fn authenticate(self, user_id: u64, _password: &str) -> Session<Authenticated> {
// In reality: verify password hash
Session { user_id, _auth: PhantomData }
}
}
impl Session<Authenticated> {
pub fn user_id(&self) -> u64 { self.user_id }
// Only authenticated sessions can access protected resources
pub fn access_profile(&self) -> String {
format!("Profile for user {}", self.user_id)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sort_witness_is_sorted() {
let s = Sorted::sort(vec![3, 1, 4, 1, 5, 9]);
let v = s.as_slice();
for w in v.windows(2) {
assert!(w[0] <= w[1], "not sorted: {:?}", v);
}
}
#[test]
fn binary_search_found() {
let s = Sorted::sort(vec![1, 2, 3, 4, 5]);
assert!(s.binary_search(&3));
}
#[test]
fn binary_search_not_found() {
let s = Sorted::sort(vec![1, 2, 4, 5]);
assert!(!s.binary_search(&3));
}
#[test]
fn merge_preserves_sorted_invariant() {
let a = Sorted::sort(vec![1, 3, 5]);
let b = Sorted::sort(vec![2, 4, 6]);
let m = a.merge(b);
let v = m.as_slice();
for w in v.windows(2) {
assert!(w[0] <= w[1]);
}
assert_eq!(v, &[1, 2, 3, 4, 5, 6]);
}
#[test]
fn non_zero_rejects_zero() {
assert!(NonZeroU64::new(0).is_none());
assert!(NonZeroU64::new(1).is_some());
}
#[test]
fn non_zero_divide_no_panic() {
let d = NonZeroU64::new(7).unwrap();
assert_eq!(d.divide(49), 7);
}
}
(* 742: Type Witnesses โ OCaml GADT approach *)
(* A 'sorted list' witness โ can only be created by our sort function *)
module SortedList : sig
type 'a t
val sort : 'a list -> 'a t (* only entry point *)
val to_list : 'a t -> 'a list
val merge : 'a t -> 'a t -> 'a t (* merging sorted lists = sorted *)
val binary_search : int t -> int -> bool
end = struct
type 'a t = Sorted of 'a list (* private constructor *)
let sort lst = Sorted (List.sort compare lst)
let to_list (Sorted lst) = lst
let merge (Sorted a) (Sorted b) =
let rec merge_sorted a b = match a, b with
| [], r | r, [] -> r
| x :: xs, y :: ys ->
if x <= y then x :: merge_sorted xs (y :: ys)
else y :: merge_sorted (x :: xs) ys
in
Sorted (merge_sorted a b)
let binary_search (Sorted lst) target =
let arr = Array.of_list lst in
let n = Array.length arr in
let lo = ref 0 and hi = ref (n - 1) in
let found = ref false in
while !lo <= !hi && not !found do
let mid = (!lo + !hi) / 2 in
if arr.(mid) = target then found := true
else if arr.(mid) < target then lo := mid + 1
else hi := mid - 1
done;
!found
end
let () =
let s = SortedList.sort [5; 1; 3; 2; 4] in
Printf.printf "Sorted: [%s]\n"
(SortedList.to_list s |> List.map string_of_int |> String.concat ";");
Printf.printf "Binary search 3: %b\n" (SortedList.binary_search s 3);
Printf.printf "Binary search 6: %b\n" (SortedList.binary_search s 6);
let s2 = SortedList.sort [10; 8; 6] in
let merged = SortedList.merge s s2 in
Printf.printf "Merged: [%s]\n"
(SortedList.to_list merged |> List.map string_of_int |> String.concat ";")