๐Ÿฆ€ Functional Rust

383: Simulating Higher-Kinded Types with GATs

Difficulty: 4 Level: Expert Write generic code that works over `Option`, `Vec`, and `Result` as containers โ€” without repeating yourself โ€” using Generic Associated Types to simulate what Rust's type system can't express natively.

The Problem This Solves

You want to write a function that works for "any mappable container." In most languages this is straightforward: a `Functor` typeclass or interface that says "you can call `map` on me." In Rust, you hit a wall:
// What you WANT to write:
fn double_all<F: Functor>(container: F<i32>) -> F<i32> {
 container.map(|x| x * 2)
}

// But Rust says: ERROR โ€” can't use `F<i32>` (F is not a type constructor)
Rust's trait system only talks about concrete types, not type constructors. You can say `T: Clone` (T is some type that can be cloned), but you cannot say `F: Functor` where `F` is something like `Option` or `Vec` that still needs a type parameter. This means if you want `map` to work generically, you write it three times: once for `Option<A>`, once for `Vec<A>`, once for `Result<A, E>`. Any library function that should work for "any mappable container" gets copied for each container. Combinators that compose functors require even more boilerplate. Higher-Kinded Types (HKT) solve this by letting you abstract over type constructors, not just types. Rust doesn't have native HKT, but Generic Associated Types (GATs, stabilized in Rust 1.65) let you simulate it โ€” with some verbosity. This exists to solve exactly that pain.

The Intuition

The problem in one sentence: Rust traits can be generic over types (`T: Clone`) but not type constructors (`F<_>: Functor`). Think of the difference between a box and a box shape: Rust traits work with specific boxes. What we want is to write rules about box shapes โ€” "any box shape that has a `map` operation." GATs (Generic Associated Types) as the workaround: Instead of saying "F is a type constructor," we say "any type that has an associated type `Mapped<B>` describing what it becomes after mapping to `B`."
trait Functor {
 type Unwrapped;         // the A in Container<A>
 type Mapped<B>;         // what this becomes when you map A -> B
 
 fn map<B>(self, f: fn(Self::Unwrapped) -> B) -> Self::Mapped<B>;
}
The `Mapped<B>` is the GAT. It's an associated type that is itself generic. Before GATs (Rust < 1.65), this was impossible. With GATs, each implementor says "when you map me to B, you get [this specific type]":

How It Works in Rust

// The Functor trait โ€” simulating HKT via GATs
trait Functor {
 type Unwrapped;           // the inner type A
 type Mapped<B>;           // GAT: what we become after mapping to B

 fn map<B, F: Fn(Self::Unwrapped) -> B>(self, f: F) -> Self::Mapped<B>;
}

// Implementation for Option<A>
impl<A> Functor for Option<A> {
 type Unwrapped = A;
 type Mapped<B> = Option<B>;  // Option<A>.map(...) -> Option<B>

 fn map<B, F: Fn(A) -> B>(self, f: F) -> Option<B> {
     self.map(f)  // delegate to Option's built-in map
 }
}

// Implementation for Vec<A>
impl<A> Functor for Vec<A> {
 type Unwrapped = A;
 type Mapped<B> = Vec<B>;  // Vec<A>.map(...) -> Vec<B>

 fn map<B, F: Fn(A) -> B>(self, f: F) -> Vec<B> {
     self.into_iter().map(f).collect()
 }
}

// Implementation for Result<A, E>
impl<A, E> Functor for Result<A, E> {
 type Unwrapped = A;
 type Mapped<B> = Result<B, E>;  // Result<A,E>.map(...) -> Result<B,E>

 fn map<B, F: Fn(A) -> B>(self, f: F) -> Result<B, E> {
     self.map(f)
 }
}

fn main() {
 // All three use the SAME Functor trait โ€” generic, not repeated
 let opt: Option<i32> = Some(21);
 let doubled = Functor::map(opt, |x| x * 2);  // Some(42)

 let v: Vec<i32> = vec![1, 2, 3, 4];
 let tripled = Functor::map(v, |x| x * 3);    // [3, 6, 9, 12]

 let r: Result<i32, &str> = Ok(10);
 let stringified = Functor::map(r, |x| x.to_string()); // Ok("10")
}
The limitation: You still can't write `fn map_twice<F: Functor>(container: F) -> F` because `F` at that point is a concrete type, not a type constructor. GATs let you describe the output type, but you can't yet write fully generic code like `fn works_for_any_functor<F<_>: Functor>(...)`. That would require native HKT.

What This Unlocks

Key Differences

ConceptOCamlRust
HKTNative: `'a t` โ€” type constructor is first-classNot native; simulated via GATs (`type Mapped<B>`)
Functor`module type FUNCTOR = sig type 'a t; val fmap: ('a -> 'b) -> 'a t -> 'b t end``trait Functor { type Unwrapped; type Mapped<B>; fn map... }`
Abstraction over F`functor Make(F: FUNCTOR) = ...`Partially possible; fully generic code over F still limited
StabilityNative since OCaml 1.0GATs stabilized in Rust 1.65 (2022)
Output typeInferred from `'b t`Explicit: `Self::Mapped<B>` must be declared per impl
ComplexityModerate โ€” standard ML module systemHigh โ€” GAT lifetime constraints add verbosity
// Simulating Higher-Kinded Types with GATs in Rust
// GATs stabilized in Rust 1.65

trait Functor {
    type Unwrapped;
    type Mapped<B>; // GAT: the "higher-kinded" part

    fn map<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 map<B, F: Fn(A) -> B>(self, f: F) -> Option<B> {
        self.map(f) // delegate to Option::map
    }
}

impl<A> Functor for Vec<A> {
    type Unwrapped = A;
    type Mapped<B> = Vec<B>;

    fn map<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 map<B, F: Fn(A) -> B>(self, f: F) -> Result<B, E> {
        self.map(f)
    }
}

fn main() {
    let opt: Option<i32> = Some(21);
    let doubled = Functor::map(opt, |x| x * 2);
    println!("Option map: {:?}", doubled);

    let v: Vec<i32> = vec![1, 2, 3, 4];
    let tripled = Functor::map(v, |x| x * 3);
    println!("Vec map: {:?}", tripled);

    let r: Result<i32, &str> = Ok(10);
    let stringified = Functor::map(r, |x| x.to_string());
    println!("Result map: {:?}", stringified);
}

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

    #[test]
    fn test_option_functor() {
        let x: Option<i32> = Some(5);
        assert_eq!(Functor::map(x, |n| n * 2), Some(10));
        let none: Option<i32> = None;
        assert_eq!(Functor::map(none, |n: i32| n * 2), None);
    }

    #[test]
    fn test_vec_functor() {
        let v = vec![1i32, 2, 3];
        assert_eq!(Functor::map(v, |x| x + 1), vec![2, 3, 4]);
    }
}
(* Higher-kinded types in OCaml โ€” native support *)

module type FUNCTOR = sig
  type 'a t
  val map : ('a -> 'b) -> 'a t -> 'b t
end

module OptionFunctor : FUNCTOR with type 'a t = 'a option = struct
  type 'a t = 'a option
  let map f = function None -> None | Some x -> Some (f x)
end

module ListFunctor : FUNCTOR with type 'a t = 'a list = struct
  type 'a t = 'a list
  let map = List.map
end

(* Generic function over any functor *)
let double_in_functor (type a) (module F : FUNCTOR with type 'a t = a option)
    (x : int F.t) =
  F.map (fun n -> n * 2) x

let () =
  let opt_result = OptionFunctor.map (fun x -> x * 2) (Some 21) in
  Printf.printf "Option map: %s\n"
    (match opt_result with None -> "None" | Some x -> string_of_int x);
  let list_result = ListFunctor.map (fun x -> x * 3) [1;2;3;4] in
  Printf.printf "List map: [%s]\n"
    (String.concat "; " (List.map string_of_int list_result))