๐Ÿฆ€ Functional Rust

131: Builder Pattern with Typestate

Difficulty: โญโญโญ Level: Advanced Construct complex objects step by step with a fluent API, where `build()` only compiles after all required fields have been set.

The Problem This Solves

A struct with many fields โ€” some required, some optional โ€” is annoying to construct. You could add a `new(name, email, ...)` function, but with many fields it becomes unwieldy and easy to mix up. You could use a mutable builder with `Option` fields and check them in `build()`, but then you get runtime panics or `Result` errors for things you know at compile time: "did I call `.name()`?" The typical builder pattern solves ergonomics but not safety. Nothing stops you from writing `UserBuilder::new().age(30).build()` โ€” forgetting both `name` and `email`. You discover the error at runtime. Typestate builders solve this: the builder type tracks which fields have been set. `UserBuilder<Optional, Optional>` doesn't have a `build()` method. `UserBuilder<Required, Required>` does. If you forget to call `.name()`, the types don't add up and you get a compile error before your code ever runs. Required fields are documented in the type, not just in doc comments.

The Intuition

Take the typestate pattern (example 130) and apply it to a builder. Each "slot" in the builder โ€” name, email, URL, method โ€” is a phantom type parameter that starts as `Optional` and transitions to `Required` when you provide the value. The builder struct holds the same fields throughout. Only the type changes. `UserBuilder<Optional, Optional>` and `UserBuilder<Required, Required>` are both the same struct in memory โ€” but different types to the compiler. The `build()` method is in an `impl` block that only applies when all phantom parameters are `Required`. Each setter method takes `self`, sets the field, and returns `self` with the relevant phantom parameter flipped to `Required`. The chain builds up state in types, not in runtime flags.

How It Works in Rust

use std::marker::PhantomData;

// Phantom markers โ€” no data, just type labels
struct Required;
struct Optional;

// Builder: two phantom params track whether name and email have been set
struct UserBuilder<Name, Email> {
 name: Option<String>,
 email: Option<String>,
 age: Option<u32>,
 _phantom: PhantomData<(Name, Email)>,
}

// Start: neither field set
impl UserBuilder<Optional, Optional> {
 fn new() -> Self {
     UserBuilder { name: None, email: None, age: None, _phantom: PhantomData }
 }
}

// .name() is available on any builder where Name = Optional
// Returns a builder with Name = Required (Email stays the same)
impl<E> UserBuilder<Optional, E> {
 fn name(self, name: &str) -> UserBuilder<Required, E> {
     UserBuilder {
         name: Some(name.to_string()),
         email: self.email,
         age: self.age,
         _phantom: PhantomData,
     }
 }
}

// .email() is available on any builder where Email = Optional
impl<N> UserBuilder<N, Optional> {
 fn email(self, email: &str) -> UserBuilder<N, Required> {
     UserBuilder {
         name: self.name,
         email: Some(email.to_string()),
         age: self.age,
         _phantom: PhantomData,
     }
 }
}

// .age() is always available โ€” it's optional, so no phantom transition needed
impl<N, E> UserBuilder<N, E> {
 fn age(mut self, age: u32) -> Self {
     self.age = Some(age);
     self
 }
}

// build() ONLY exists when BOTH required fields are set
// impl<Required, Optional> does NOT have build()
impl UserBuilder<Required, Required> {
 fn build(self) -> User {
     User {
         name: self.name.unwrap(),    // safe: Required means we set it
         email: self.email.unwrap(),  // safe: Required means we set it
         age: self.age,
     }
 }
}
Usage:
// All of these compile โœ“ โ€” order doesn't matter
let user = UserBuilder::new().name("Alice").email("a@b.com").build();
let user = UserBuilder::new().email("a@b.com").name("Alice").age(30).build();

// These do NOT compile:
// UserBuilder::new().build();              // missing name and email
// UserBuilder::new().name("Alice").build(); // missing email
With const bool generics (alternative approach):
// Same idea using `const HAS_URL: bool` instead of marker structs
struct HttpRequestBuilder<const HAS_URL: bool, const HAS_METHOD: bool> { ... }

impl HttpRequestBuilder<true, true> {
 fn build(self) -> HttpRequest { ... }  // only when both are true
}

What This Unlocks

Key Differences

ConceptOCamlRust
Builder statePhantom type in record `('name, 'email) user_builder`Phantom type params `UserBuilder<Name, Email>`
Field setting`set_name : (unset, 'e) t -> (set, 'e) t``fn name(self, ...) -> UserBuilder<Required, E>`
build() gating`build : (set, set) t -> user``impl UserBuilder<Required, Required> { fn build }`
Memory overheadNone โ€” phantom types erasedNone โ€” `PhantomData` is zero-sized
// Example 131: Builder Pattern with Typestate
use std::marker::PhantomData;

// Approach 1: Typestate builder โ€” required fields enforced at compile time
struct Required;
struct Optional;

struct UserBuilder<Name, Email> {
    name: Option<String>,
    email: Option<String>,
    age: Option<u32>,
    _phantom: PhantomData<(Name, Email)>,
}

#[derive(Debug, PartialEq)]
struct User {
    name: String,
    email: String,
    age: Option<u32>,
}

impl UserBuilder<Optional, Optional> {
    fn new() -> Self {
        UserBuilder {
            name: None, email: None, age: None,
            _phantom: PhantomData,
        }
    }
}

impl<E> UserBuilder<Optional, E> {
    fn name(self, name: &str) -> UserBuilder<Required, E> {
        UserBuilder {
            name: Some(name.to_string()),
            email: self.email,
            age: self.age,
            _phantom: PhantomData,
        }
    }
}

impl<N> UserBuilder<N, Optional> {
    fn email(self, email: &str) -> UserBuilder<N, Required> {
        UserBuilder {
            name: self.name,
            email: Some(email.to_string()),
            age: self.age,
            _phantom: PhantomData,
        }
    }
}

impl<N, E> UserBuilder<N, E> {
    fn age(mut self, age: u32) -> Self {
        self.age = Some(age);
        self
    }
}

// build() only available when BOTH required fields are set
impl UserBuilder<Required, Required> {
    fn build(self) -> User {
        User {
            name: self.name.unwrap(),
            email: self.email.unwrap(),
            age: self.age,
        }
    }
}

// Approach 2: Generic builder with const bool tracking
struct HttpRequestBuilder<const HAS_URL: bool, const HAS_METHOD: bool> {
    url: Option<String>,
    method: Option<String>,
    headers: Vec<(String, String)>,
}

impl HttpRequestBuilder<false, false> {
    fn new() -> Self {
        HttpRequestBuilder { url: None, method: None, headers: vec![] }
    }
}

impl<const M: bool> HttpRequestBuilder<false, M> {
    fn url(self, url: &str) -> HttpRequestBuilder<true, M> {
        HttpRequestBuilder { url: Some(url.to_string()), method: self.method, headers: self.headers }
    }
}

impl<const U: bool> HttpRequestBuilder<U, false> {
    fn method(self, method: &str) -> HttpRequestBuilder<U, true> {
        HttpRequestBuilder { url: self.url, method: Some(method.to_string()), headers: self.headers }
    }
}

impl<const U: bool, const M: bool> HttpRequestBuilder<U, M> {
    fn header(mut self, key: &str, value: &str) -> Self {
        self.headers.push((key.to_string(), value.to_string()));
        self
    }
}

#[derive(Debug)]
struct HttpRequest { url: String, method: String, headers: Vec<(String, String)> }

impl HttpRequestBuilder<true, true> {
    fn build(self) -> HttpRequest {
        HttpRequest {
            url: self.url.unwrap(),
            method: self.method.unwrap(),
            headers: self.headers,
        }
    }
}

fn main() {
    let user = UserBuilder::new()
        .name("Alice")
        .email("alice@example.com")
        .age(30)
        .build();
    println!("User: {:?}", user);

    let req = HttpRequestBuilder::new()
        .url("https://api.example.com")
        .method("GET")
        .header("Authorization", "Bearer token")
        .build();
    println!("Request: {:?}", req);
}

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

    #[test]
    fn test_user_builder_all_fields() {
        let user = UserBuilder::new()
            .name("Alice")
            .email("alice@test.com")
            .age(25)
            .build();
        assert_eq!(user.name, "Alice");
        assert_eq!(user.email, "alice@test.com");
        assert_eq!(user.age, Some(25));
    }

    #[test]
    fn test_user_builder_required_only() {
        let user = UserBuilder::new()
            .email("bob@test.com")
            .name("Bob")
            .build();
        assert_eq!(user.name, "Bob");
        assert_eq!(user.age, None);
    }

    #[test]
    fn test_http_builder() {
        let req = HttpRequestBuilder::new()
            .method("POST")
            .url("https://api.test.com")
            .header("Content-Type", "application/json")
            .build();
        assert_eq!(req.url, "https://api.test.com");
        assert_eq!(req.method, "POST");
        assert_eq!(req.headers.len(), 1);
    }
}
(* Example 131: Builder Pattern with Typestate *)

(* Approach 1: GADT builder requiring fields *)
type unset = Unset_t
type set = Set_t

type ('name, 'email) user_builder = {
  name : string option;
  email : string option;
  age : int option;
}

let empty_builder : (unset, unset) user_builder =
  { name = None; email = None; age = None }

let set_name name (b : (unset, 'e) user_builder) : (set, 'e) user_builder =
  { b with name = Some name }

let set_email email (b : ('n, unset) user_builder) : ('n, set) user_builder =
  { b with email = Some email }

let set_age age b = { b with age = Some age }

type user = { user_name : string; user_email : string; user_age : int option }

let build (b : (set, set) user_builder) : user =
  { user_name = Option.get b.name;
    user_email = Option.get b.email;
    user_age = b.age }

(* Approach 2: Module-based builder *)
module type HAS_NAME = sig val name : string end
module type HAS_EMAIL = sig val email : string end

module UserBuilder = struct
  type t = { name: string; email: string; age: int option }
  let create name email = { name; email; age = None }
  let with_age age t = { t with age = Some age }
end

(* Approach 3: Continuation-style builder *)
let build_user ~name ~email ?age () =
  { user_name = name; user_email = email; user_age = age }

(* Tests *)
let () =
  let u = empty_builder
    |> set_name "Alice"
    |> set_email "alice@example.com"
    |> set_age 30
    |> build in
  assert (u.user_name = "Alice");
  assert (u.user_email = "alice@example.com");
  assert (u.user_age = Some 30);

  let u2 = build_user ~name:"Bob" ~email:"bob@test.com" () in
  assert (u2.user_name = "Bob");
  assert (u2.user_age = None);

  Printf.printf "โœ“ All tests passed\n"

๐Ÿ“Š Detailed Comparison

Comparison: Builder Pattern with Typestate

OCaml

๐Ÿช Show OCaml equivalent
type ('name, 'email) user_builder = {
name : string option;
email : string option;
}

let set_name name (b : (unset, 'e) user_builder) : (set, 'e) user_builder =
{ b with name = Some name }

let build (b : (set, set) user_builder) : user =
{ user_name = Option.get b.name; user_email = Option.get b.email }

Rust

impl UserBuilder<Optional, Optional> {
 fn new() -> Self { /* ... */ }
}

impl<E> UserBuilder<Optional, E> {
 fn name(self, name: &str) -> UserBuilder<Required, E> { /* ... */ }
}

// Only available when both Required
impl UserBuilder<Required, Required> {
 fn build(self) -> User { /* ... */ }
}