Type-Level Booleans
Tutorial Video
Text description (accessibility)
This video demonstrates the "Type-Level Booleans" functional Rust example. Difficulty level: Advanced. Key concepts covered: Type System, Phantom Types, Compile-Time Safety. Encode `true`/`false` as compile-time *types* rather than runtime values, so the compiler can enforce logical constraints (e.g., "both validated AND logging must be enabled before calling `execute()`") without any runtime checks or panics. Key difference from OCaml: 1. **Representation:** OCaml phantom types use a single record with an ignored field; Rust uses `PhantomData<T>` which compiles to nothing.
Tutorial
The Problem
Encode true/false as compile-time types rather than runtime values, so the compiler can enforce logical constraints (e.g., "both validated AND logging must be enabled before calling execute()") without any runtime checks or panics.
🎯 Learning Outcomes
struct True; struct False;) carry type-level information with zero runtime costPhantomData<T> lets a generic struct hold a type parameter that has no corresponding fieldimpl Config<True, True>) turns missing setup steps into compile errorstrait Not { type Output: Bool }) encode type-level logic that the compiler evaluates statically🦀 The Rust Way
Rust uses empty structs (struct True; and struct False;) as type-level labels and PhantomData<V> to include a phantom type parameter in a struct without storing data. Methods are gated by writing impl Config<True, True> — only the fully-setup instantiation exposes execute(). Any attempt to call it prematurely is a compile-time type error, not a runtime panic.
Code Example
use std::marker::PhantomData;
pub struct True;
pub struct False;
pub trait Bool { const VALUE: bool; }
impl Bool for True { const VALUE: bool = true; }
impl Bool for False { const VALUE: bool = false; }
// Type-level NOT via associated type
pub trait Not { type Output: Bool; }
impl Not for True { type Output = False; }
impl Not for False { type Output = True; }Key Differences
PhantomData<T> which compiles to nothing.trait And<B> { type Output: Bool }).OCaml Approach
OCaml uses phantom type parameters — a type variable that appears in the type signature but not in the data representation. type 'b flag = { _phantom : unit } is a record whose field carries no information; only the type parameter 'b distinguishes true_t flag from false_t flag. Module signatures hide constructors so callers cannot forge an invalid state.
Full Source
#![allow(clippy::all)]
//! Example 128: Type-Level Booleans
//!
//! Encode `true`/`false` as *types* instead of values so the compiler enforces
//! logical constraints without any runtime checks.
use std::marker::PhantomData;
// ── Approach 1: Marker structs ────────────────────────────────────────────────
// Two zero-sized structs that act as compile-time labels.
pub struct True;
pub struct False;
/// Lift a type-level boolean to a runtime value.
pub trait Bool {
const VALUE: bool;
}
impl Bool for True {
const VALUE: bool = true;
}
impl Bool for False {
const VALUE: bool = false;
}
// ── Type-level NOT ────────────────────────────────────────────────────────────
pub trait Not {
type Output: Bool;
}
impl Not for True {
type Output = False;
}
impl Not for False {
type Output = True;
}
// ── Type-level AND ────────────────────────────────────────────────────────────
pub trait And<B: Bool> {
type Output: Bool;
}
impl<B: Bool> And<B> for True {
type Output = B; // True AND B = B
}
impl<B: Bool> And<B> for False {
type Output = False; // False AND _ = False
}
// ── Type-level OR ─────────────────────────────────────────────────────────────
pub trait Or<B: Bool> {
type Output: Bool;
}
impl<B: Bool> Or<B> for True {
type Output = True; // True OR _ = True
}
impl<B: Bool> Or<B> for False {
type Output = B; // False OR B = B
}
// ── Approach 2: Builder enforced at compile time ───────────────────────────────
//
// `Config<Validated, Logged>` where each type parameter is either `True` or
// `False`. The `execute()` method is defined *only* on `Config<True, True>`,
// so calling it before completing both setup steps is a compile error — the
// method simply doesn't exist on the other variants.
pub struct Config<V, L> {
pub host: String,
pub port: u16,
// PhantomData holds the type parameters without storing any data at runtime.
_validated: PhantomData<V>,
_logged: PhantomData<L>,
}
impl Config<False, False> {
pub fn new(host: impl Into<String>, port: u16) -> Self {
Config {
host: host.into(),
port,
_validated: PhantomData,
_logged: PhantomData,
}
}
}
// validate() is available whenever V = False (transitions V: False → True).
impl<L> Config<False, L> {
pub fn validate(self) -> Config<True, L> {
Config {
host: self.host,
port: self.port,
_validated: PhantomData,
_logged: PhantomData,
}
}
}
// enable_logging() is available whenever L = False (transitions L: False → True).
impl<V> Config<V, False> {
pub fn enable_logging(self) -> Config<V, True> {
Config {
host: self.host,
port: self.port,
_validated: PhantomData,
_logged: PhantomData,
}
}
}
// execute() only exists on the fully-configured type.
impl Config<True, True> {
pub fn execute(&self) -> String {
format!("Executing on {}:{}", self.host, self.port)
}
}
// ── Approach 3: Tagged value — attach a type-level boolean to any value ────────
pub struct Tagged<T, B> {
pub value: T,
_marker: PhantomData<B>,
}
impl<T, B: Bool> Tagged<T, B> {
pub fn new(value: T) -> Self {
Tagged {
value,
_marker: PhantomData,
}
}
pub fn is_true() -> bool {
B::VALUE
}
}
// get_verified() only compiles when the tag is True.
impl<T> Tagged<T, True> {
pub fn get_verified(&self) -> &T {
&self.value
}
}
// ── Tests ─────────────────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bool_values() {
assert!(True::VALUE);
assert!(!False::VALUE);
}
#[test]
fn test_not() {
assert_eq!(<True as Not>::Output::VALUE, false);
assert_eq!(<False as Not>::Output::VALUE, true);
}
#[test]
fn test_and() {
assert_eq!(<True as And<True>>::Output::VALUE, true);
assert_eq!(<True as And<False>>::Output::VALUE, false);
assert_eq!(<False as And<True>>::Output::VALUE, false);
assert_eq!(<False as And<False>>::Output::VALUE, false);
}
#[test]
fn test_or() {
assert_eq!(<True as Or<True>>::Output::VALUE, true);
assert_eq!(<True as Or<False>>::Output::VALUE, true);
assert_eq!(<False as Or<True>>::Output::VALUE, true);
assert_eq!(<False as Or<False>>::Output::VALUE, false);
}
#[test]
fn test_config_validate_then_log() {
let result = Config::new("localhost", 8080)
.validate()
.enable_logging()
.execute();
assert_eq!(result, "Executing on localhost:8080");
}
#[test]
fn test_config_log_then_validate() {
// Order of setup steps doesn't matter — both paths reach Config<True, True>.
let result = Config::new("example.com", 443)
.enable_logging()
.validate()
.execute();
assert_eq!(result, "Executing on example.com:443");
}
#[test]
fn test_tagged_true() {
let v: Tagged<i32, True> = Tagged::new(42);
assert_eq!(*v.get_verified(), 42);
assert!(Tagged::<i32, True>::is_true());
}
#[test]
fn test_tagged_false() {
let v: Tagged<&str, False> = Tagged::new("hello");
assert_eq!(v.value, "hello");
assert!(!Tagged::<i32, False>::is_true());
}
#[test]
fn test_bool_const_evaluation() {
// Verify that Bool::VALUE can be used in const contexts.
const T: bool = True::VALUE;
const F: bool = False::VALUE;
assert!(T);
assert!(!F);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bool_values() {
assert!(True::VALUE);
assert!(!False::VALUE);
}
#[test]
fn test_not() {
assert_eq!(<True as Not>::Output::VALUE, false);
assert_eq!(<False as Not>::Output::VALUE, true);
}
#[test]
fn test_and() {
assert_eq!(<True as And<True>>::Output::VALUE, true);
assert_eq!(<True as And<False>>::Output::VALUE, false);
assert_eq!(<False as And<True>>::Output::VALUE, false);
assert_eq!(<False as And<False>>::Output::VALUE, false);
}
#[test]
fn test_or() {
assert_eq!(<True as Or<True>>::Output::VALUE, true);
assert_eq!(<True as Or<False>>::Output::VALUE, true);
assert_eq!(<False as Or<True>>::Output::VALUE, true);
assert_eq!(<False as Or<False>>::Output::VALUE, false);
}
#[test]
fn test_config_validate_then_log() {
let result = Config::new("localhost", 8080)
.validate()
.enable_logging()
.execute();
assert_eq!(result, "Executing on localhost:8080");
}
#[test]
fn test_config_log_then_validate() {
// Order of setup steps doesn't matter — both paths reach Config<True, True>.
let result = Config::new("example.com", 443)
.enable_logging()
.validate()
.execute();
assert_eq!(result, "Executing on example.com:443");
}
#[test]
fn test_tagged_true() {
let v: Tagged<i32, True> = Tagged::new(42);
assert_eq!(*v.get_verified(), 42);
assert!(Tagged::<i32, True>::is_true());
}
#[test]
fn test_tagged_false() {
let v: Tagged<&str, False> = Tagged::new("hello");
assert_eq!(v.value, "hello");
assert!(!Tagged::<i32, False>::is_true());
}
#[test]
fn test_bool_const_evaluation() {
// Verify that Bool::VALUE can be used in const contexts.
const T: bool = True::VALUE;
const F: bool = False::VALUE;
assert!(T);
assert!(!F);
}
}
Deep Comparison
OCaml vs Rust: Type-Level Booleans
Side-by-Side Code
OCaml
(* Phantom types: 'b is never stored — it's only a compile-time label *)
type true_t = True_t
type false_t = False_t
type 'b flag = { _phantom : unit }
let mk_true : true_t flag = { _phantom = () }
let mk_false : false_t flag = { _phantom = () }
(* GADT-based type-level bool *)
type _ tbool =
| TTrue : true_t tbool
| TFalse : false_t tbool
Rust (marker structs + trait)
use std::marker::PhantomData;
pub struct True;
pub struct False;
pub trait Bool { const VALUE: bool; }
impl Bool for True { const VALUE: bool = true; }
impl Bool for False { const VALUE: bool = false; }
// Type-level NOT via associated type
pub trait Not { type Output: Bool; }
impl Not for True { type Output = False; }
impl Not for False { type Output = True; }
Rust (builder enforced at compile time)
pub struct Config<V, L> {
host: String,
port: u16,
_validated: PhantomData<V>,
_logged: PhantomData<L>,
}
// execute() only exists on Config<True, True>
impl Config<True, True> {
pub fn execute(&self) -> String {
format!("Executing on {}:{}", self.host, self.port)
}
}
Type Signatures
| Concept | OCaml | Rust |
|---|---|---|
| Type-level true | true_t (phantom param) | struct True; |
| Type-level false | false_t (phantom param) | struct False; |
| Phantom parameter | 'b flag — 'b unused in body | PhantomData<B> |
| Runtime reflection | Module value val value : bool | trait Bool { const VALUE: bool } |
| Type-level NOT | Separate type families | trait Not { type Output: Bool } |
| Conditional methods | Module functors / GADTs | impl Config<True, True> |
Key Insights
struct True; compiles to nothing at runtime, exactly like OCaml's type true_t = True_t. Both are erased before execution.type _ tbool) prove relationships between type indices at match sites. Rust's trait Not { type Output } encodes the same logic as a compiler-verified type mapping.impl specialization enforces preconditions** — defining execute() only on Config<True, True> means calling it prematurely is a compile error, not a runtime panic. OCaml achieves this with module signatures that hide the constructor.PhantomData prevents variance surprises** — Rust's ownership model requires declaring how a phantom type is used (owned, borrowed, covariant, etc.). PhantomData<V> tells the compiler Config is covariant over V and owns a notional V, which is the correct variance for a state-machine type.When to Use Each Style
**Use idiomatic Rust (Bool trait + const VALUE) when:** you need to inspect the boolean at runtime (e.g., logging, serialization) while still encoding it as a type.
**Use the builder (impl Config<True, True>) when:** you want compile-time enforcement of a multi-step setup protocol with no runtime cost whatsoever — the type itself becomes the proof.
Exercises
And, Or, and Not operations on your True/False types and write tests that verify them at compile time using trait bounds.Succ<Succ<Zero>> for two, and implement type-level Add that resolves to the correct type at compile time.CanRead, CanWrite marker types and restrict API methods to only compile when the phantom type parameter satisfies the required capability trait.