383: Simulating Higher-Kinded Types with GATs
Tutorial Video
Text description (accessibility)
This video demonstrates the "383: Simulating Higher-Kinded Types with GATs" functional Rust example. Difficulty level: Advanced. Key concepts covered: Functional Programming. Higher-Kinded Types (HKTs) let you abstract over type constructors like `Option`, `Vec`, and `Result` — not just concrete types. Key difference from OCaml: 1. **Native vs. simulated**: OCaml supports `'a t` (type constructor abstraction) natively; Rust requires the GAT workaround with `type Mapped<B>`.
Tutorial
The Problem
Higher-Kinded Types (HKTs) let you abstract over type constructors like Option, Vec, and Result — not just concrete types. A Functor typeclass in Haskell captures "any container that can be mapped over" without specifying whether it's a list, maybe, or tree. Rust lacks native HKTs, but Generic Associated Types (GATs, stabilized in Rust 1.65) enable simulation: type Mapped<B> as an associated type lets a trait express "map this container's inner type from A to B."
This pattern appears in functional library design (the futures crate's Output associated type, async-trait patterns) and is the foundation of monadic abstractions used in parser combinators and effect systems.
🎯 Learning Outcomes
Functor trait implemented for Option, Vec, and ResultCode Example
#![allow(clippy::all)]
//! Simulating Higher-Kinded Types with GATs
pub trait Functor {
type Unwrapped;
type Mapped<B>;
fn fmap<B, F: Fn(Self::Unwrapped) -> B>(self, f: F) -> Self::Mapped<B>;
}
impl<A> Functor for Option<A> {
type Unwrapped = A;
type Mapped<B> = Option<B>;
fn fmap<B, F: Fn(A) -> B>(self, f: F) -> Option<B> {
self.map(f)
}
}
impl<A> Functor for Vec<A> {
type Unwrapped = A;
type Mapped<B> = Vec<B>;
fn fmap<B, F: Fn(A) -> B>(self, f: F) -> Vec<B> {
self.into_iter().map(f).collect()
}
}
impl<A, E> Functor for Result<A, E> {
type Unwrapped = A;
type Mapped<B> = Result<B, E>;
fn fmap<B, F: Fn(A) -> B>(self, f: F) -> Result<B, E> {
self.map(f)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_option_functor() {
let opt = Some(21);
let doubled = opt.fmap(|x| x * 2);
assert_eq!(doubled, Some(42));
}
#[test]
fn test_vec_functor() {
let v = vec![1, 2, 3];
let tripled = v.fmap(|x| x * 3);
assert_eq!(tripled, vec![3, 6, 9]);
}
#[test]
fn test_result_functor() {
let r: Result<i32, &str> = Ok(10);
let s = r.fmap(|x| x.to_string());
assert_eq!(s, Ok("10".to_string()));
}
#[test]
fn test_none_functor() {
let opt: Option<i32> = None;
let result = opt.fmap(|x| x * 2);
assert_eq!(result, None);
}
}Key Differences
'a t (type constructor abstraction) natively; Rust requires the GAT workaround with type Mapped<B>.fmap f opt reads naturally; Rust's opt.fmap(f) works but GAT bounds can become verbose in generic contexts.bind as naturally as fmap; Rust's GAT approach struggles with bind (flatMap) since Mapped<Mapped<B>> creates problematic nesting.OCaml Approach
OCaml supports HKTs natively through its module system. A Functor module signature contains type 'a t and val fmap : ('a -> 'b) -> 'a t -> 'b t. Implementing for Option is module OptionFunctor = struct type 'a t = 'a option; let fmap f = Option.map f end. The type parameter 'a in 'a t directly expresses the kind * -> * that Rust approximates with GATs. OCaml's approach is simpler and more expressive.
Full Source
#![allow(clippy::all)]
//! Simulating Higher-Kinded Types with GATs
pub trait Functor {
type Unwrapped;
type Mapped<B>;
fn fmap<B, F: Fn(Self::Unwrapped) -> B>(self, f: F) -> Self::Mapped<B>;
}
impl<A> Functor for Option<A> {
type Unwrapped = A;
type Mapped<B> = Option<B>;
fn fmap<B, F: Fn(A) -> B>(self, f: F) -> Option<B> {
self.map(f)
}
}
impl<A> Functor for Vec<A> {
type Unwrapped = A;
type Mapped<B> = Vec<B>;
fn fmap<B, F: Fn(A) -> B>(self, f: F) -> Vec<B> {
self.into_iter().map(f).collect()
}
}
impl<A, E> Functor for Result<A, E> {
type Unwrapped = A;
type Mapped<B> = Result<B, E>;
fn fmap<B, F: Fn(A) -> B>(self, f: F) -> Result<B, E> {
self.map(f)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_option_functor() {
let opt = Some(21);
let doubled = opt.fmap(|x| x * 2);
assert_eq!(doubled, Some(42));
}
#[test]
fn test_vec_functor() {
let v = vec![1, 2, 3];
let tripled = v.fmap(|x| x * 3);
assert_eq!(tripled, vec![3, 6, 9]);
}
#[test]
fn test_result_functor() {
let r: Result<i32, &str> = Ok(10);
let s = r.fmap(|x| x.to_string());
assert_eq!(s, Ok("10".to_string()));
}
#[test]
fn test_none_functor() {
let opt: Option<i32> = None;
let result = opt.fmap(|x| x * 2);
assert_eq!(result, None);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_option_functor() {
let opt = Some(21);
let doubled = opt.fmap(|x| x * 2);
assert_eq!(doubled, Some(42));
}
#[test]
fn test_vec_functor() {
let v = vec![1, 2, 3];
let tripled = v.fmap(|x| x * 3);
assert_eq!(tripled, vec![3, 6, 9]);
}
#[test]
fn test_result_functor() {
let r: Result<i32, &str> = Ok(10);
let s = r.fmap(|x| x.to_string());
assert_eq!(s, Ok("10".to_string()));
}
#[test]
fn test_none_functor() {
let opt: Option<i32> = None;
let result = opt.fmap(|x| x * 2);
assert_eq!(result, None);
}
}
Deep Comparison
OCaml vs Rust: HKT
Exercises
fn ap<B, F: Fn(Self::Unwrapped) -> B>(self, f: Self::Mapped<F>) -> Self::Mapped<B> and implement it for Option (None when either is None).Tree<T> type (leaf or node with two children) and implement Functor for it, applying fmap recursively to transform all leaf values.Foldable trait with fn fold<B, F: Fn(B, Self::Unwrapped) -> B>(self, init: B, f: F) -> B and implement it for Option and Vec.