🦀 Functional Rust

623: Grand Tour — Functional Programming Patterns in Production Rust

Difficulty: 5 Level: Master A complete worked example synthesising the entire functional Rust curriculum: domain types, pure functions, validation, error propagation, iterator pipelines, and composable transformation chains.

The Problem This Solves

Learning individual patterns in isolation is necessary but not sufficient. The real skill is combining them: algebraic data types that model the domain, pure functions that transform values without side effects, `Result` and `Option` chains that propagate errors without exceptions, and iterator pipelines that process collections without intermediate allocations — all working together in a coherent system. This example builds an order processing pipeline that a real production system might use. It touches every major functional pattern: `enum`-based error types with `Display`, pure transformation functions, `Result`-based validation, iterator chaining with `flat_map` and `fold`, currency conversion as a pure function, and `HashMap` aggregation. No `unsafe`, no `Arc<Mutex<>>`, no side effects in the core logic — functional purity throughout. This is where the entire series converges. Every pattern from examples 001–622 exists to make code like this possible: correct, composable, testable without mocks, and readable without tracing control flow through mutations.

The Intuition

Functional programming in Rust is not about avoiding `mut` or pretending Rust is Haskell. It's about a discipline of design: 1. Model the domain in types. If an illegal state is unrepresentable in the type system, you can't create it by accident. 2. Separate pure computation from side effects. Functions that transform data are easy to test; functions that print, write to disk, or mutate global state are not. 3. Use `Result` and `Option` for control flow. The `?` operator threads errors through call chains without `try/catch` or `null` checks. 4. Compose with iterators. An iterator pipeline is a data transformation expressed as a sequence of small, named, testable steps.

How It Works in Rust

// ── Domain types: illegal states are unrepresentable ─────────────────────
#[derive(Debug, Clone, Copy, PartialEq)]
enum Currency { USD, EUR, GBP }

#[derive(Debug, Clone)]
struct Order { id: String, items: Vec<OrderItem>, discount: f64 }

// ── Error type: all failure modes named and structured ───────────────────
#[derive(Debug)]
enum OrderError {
 InsufficientStock { product: String, requested: u32, available: u32 },
 InvalidDiscount(f64),
 EmptyOrder,
}

// ── Pure functions: no side effects, fully testable ──────────────────────
fn item_subtotal(item: &OrderItem) -> f64 {
 item.product.price.amount * item.qty as f64
}

fn order_total(order: &Order) -> f64 {
 order.items.iter().map(item_subtotal).sum::<f64>() * (1.0 - order.discount)
}

// ── Validation: Result propagation with ? ────────────────────────────────
fn validate_order(order: &Order) -> Result<(), OrderError> {
 if order.items.is_empty() { return Err(OrderError::EmptyOrder); }
 if order.discount < 0.0 || order.discount > 1.0 {
     return Err(OrderError::InvalidDiscount(order.discount));
 }
 for item in &order.items {
     if item.qty > item.product.stock {
         return Err(OrderError::InsufficientStock {
             product: item.product.name.clone(),
             requested: item.qty,
             available: item.product.stock,
         });
     }
 }
 Ok(())
}

// ── Pipeline: validate → transform → aggregate ───────────────────────────
fn process_orders(orders: &[Order]) -> (Vec<&Order>, Vec<(&Order, OrderError)>) {
 let (valid, invalid): (Vec<_>, Vec<_>) = orders.iter()
     .map(|o| validate_order(o).map(|_| o).map_err(|e| (o, e)))
     .partition(Result::is_ok);
 (valid.into_iter().map(|r| r.unwrap()).collect(),
  invalid.into_iter().map(|r| r.unwrap_err()).collect())
}

// ── Aggregation: iterator fold into HashMap ──────────────────────────────
fn revenue_by_currency(orders: &[&Order]) -> HashMap<String, f64> {
 orders.iter().fold(HashMap::new(), |mut acc, order| {
     let currency = order.items.first()
         .map(|i| format!("{:?}", i.product.price.currency))
         .unwrap_or_default();
     *acc.entry(currency).or_insert(0.0) += order_total(order);
     acc
 })
}
Each function is: small, named, pure (no side effects), and testable in isolation. The pipeline combines them without ceremony — no class hierarchies, no dependency injection, no mocks needed.

What This Unlocks

Key Differences

ConceptOCamlRust
Algebraic data types`type error = InsufficientStock of ...``enum OrderError { InsufficientStock { ... } }`
Pattern matching`match e with``match e { ... }` — exhaustive, same semantics
Error propagation`Result.bind`, `let*``?` operator — desugar to `match` + early return
Iterator pipeline`List.filter_map`, `List.fold_left``.filter_map()`, `.fold()`, `.partition()`
Pure functionsDefault (immutable by default)Default (move semantics; `mut` is explicit opt-in)
use std::collections::HashMap;

// ── Domain Types ─────────────────────────────────────────────────────────────
#[derive(Debug,Clone,Copy,PartialEq)]
enum Currency { USD, EUR, GBP }

#[derive(Debug,Clone)]
struct Price { amount: f64, currency: Currency }

#[derive(Debug,Clone)]
struct Product { id: String, name: String, price: Price, stock: u32 }

#[derive(Debug,Clone)]
struct OrderItem { product: Product, qty: u32 }

#[derive(Debug,Clone)]
struct Order { id: String, items: Vec<OrderItem>, discount: f64 }

// ── Error Types ───────────────────────────────────────────────────────────────
#[derive(Debug)]
enum OrderError {
    InsufficientStock { product: String, requested: u32, available: u32 },
    InvalidDiscount(f64),
    EmptyOrder,
}
impl std::fmt::Display for OrderError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            OrderError::InsufficientStock{product,requested,available} =>
                write!(f, "{}: need {}, have {}", product, requested, available),
            OrderError::InvalidDiscount(d) => write!(f, "discount {} out of [0,1]", d),
            OrderError::EmptyOrder => write!(f, "empty order"),
        }
    }
}

// ── Pure Functions ────────────────────────────────────────────────────────────
fn item_subtotal(item: &OrderItem) -> f64 {
    item.product.price.amount * item.qty as f64
}

fn order_subtotal(order: &Order) -> f64 {
    order.items.iter().map(item_subtotal).sum()
}

fn order_total(order: &Order) -> f64 {
    order_subtotal(order) * (1.0 - order.discount)
}

// ── Validation ────────────────────────────────────────────────────────────────
fn validate_order(order: &Order) -> Result<(), OrderError> {
    if order.items.is_empty() { return Err(OrderError::EmptyOrder); }
    if order.discount < 0.0 || order.discount > 1.0 {
        return Err(OrderError::InvalidDiscount(order.discount));
    }
    for item in &order.items {
        if item.qty > item.product.stock {
            return Err(OrderError::InsufficientStock {
                product: item.product.name.clone(),
                requested: item.qty,
                available: item.product.stock,
            });
        }
    }
    Ok(())
}

// ── Builder Pattern ───────────────────────────────────────────────────────────
#[derive(Default)]
struct OrderBuilder { id: String, items: Vec<OrderItem>, discount: f64 }

impl OrderBuilder {
    fn id(mut self, id: impl Into<String>) -> Self { self.id = id.into(); self }
    fn add_item(mut self, item: OrderItem) -> Self { self.items.push(item); self }
    fn discount(mut self, d: f64) -> Self { self.discount = d; self }
    fn build(self) -> Order { Order { id:self.id, items:self.items, discount:self.discount } }
}

// ── Command Pattern ───────────────────────────────────────────────────────────
#[derive(Debug,Clone)]
enum OrderCommand {
    AddItem(OrderItem),
    RemoveItem(String),
    ApplyDiscount(f64),
    Submit,
}

fn apply_command(mut order: Order, cmd: OrderCommand) -> Result<Order, OrderError> {
    match cmd {
        OrderCommand::AddItem(item) => { order.items.push(item); Ok(order) }
        OrderCommand::RemoveItem(id) => {
            order.items.retain(|i| i.product.id != id);
            Ok(order)
        }
        OrderCommand::ApplyDiscount(d) => {
            if d < 0.0 || d > 1.0 { return Err(OrderError::InvalidDiscount(d)); }
            order.discount = d;
            Ok(order)
        }
        OrderCommand::Submit => {
            validate_order(&order)?;
            Ok(order)
        }
    }
}

// ── Iterator chains (FP data processing) ─────────────────────────────────────
fn summarize_by_currency(orders: &[Order]) -> HashMap<String, f64> {
    orders.iter()
        .flat_map(|o| o.items.iter().map(|i| {
            let currency = format!("{:?}", i.product.price.currency);
            (currency, item_subtotal(i))
        }))
        .fold(HashMap::new(), |mut map, (c, amount)| {
            *map.entry(c).or_default() += amount;
            map
        })
}

fn main() {
    let widget = Product { id:"W1".into(), name:"Widget".into(), price:Price{amount:9.99,currency:Currency::USD}, stock:10 };
    let gadget = Product { id:"G1".into(), name:"Gadget".into(), price:Price{amount:24.99,currency:Currency::USD}, stock:2 };

    // Build order with builder pattern
    let order = OrderBuilder::default()
        .id("ORD-001")
        .add_item(OrderItem{product:widget.clone(), qty:2})
        .add_item(OrderItem{product:gadget.clone(), qty:1})
        .discount(0.10)
        .build();

    println!("=== Order {} ===", order.id);
    for item in &order.items {
        println!("  {} x{} = ${:.2}", item.product.name, item.qty, item_subtotal(item));
    }
    println!("  subtotal: ${:.2}", order_subtotal(&order));
    println!("  discount: {:.0}%", order.discount*100.0);
    println!("  total:    ${:.2}", order_total(&order));

    // Validate order
    match validate_order(&order) {
        Ok(()) => println!("  ✓ Valid"),
        Err(e) => println!("  ✗ Error: {}", e),
    }

    // Command pattern: process commands
    let empty = Order { id:"ORD-002".into(), items:vec![], discount:0.0 };
    let commands = vec![
        OrderCommand::AddItem(OrderItem{product:widget.clone(), qty:1}),
        OrderCommand::ApplyDiscount(0.05),
        OrderCommand::Submit,
    ];
    let result = commands.into_iter().try_fold(empty, apply_command);
    match result {
        Ok(o)  => println!("
Command-built order total: ${:.2}", order_total(&o)),
        Err(e) => println!("
Command error: {}", e),
    }

    // Invalid stock order
    let bad_order = OrderBuilder::default()
        .id("BAD-001")
        .add_item(OrderItem{product:gadget, qty:100})  // 100 > stock of 2
        .build();
    match validate_order(&bad_order) {
        Ok(()) => println!("Expected error!"),
        Err(e) => println!("
Expected error: {}", e),
    }

    // FP data processing
    let orders = vec![order.clone()];
    let summary = summarize_by_currency(&orders);
    for (currency, total) in &summary {
        println!("
{} total: ${:.2}", currency, total);
    }

    // Pure iterator pipeline: top 5 most expensive items
    let mut items: Vec<(&str, f64)> = order.items.iter()
        .map(|i| (i.product.name.as_str(), item_subtotal(i)))
        .collect();
    items.sort_by(|a,b| b.1.partial_cmp(&a.1).unwrap());
    println!("
Items by cost:");
    for (name, cost) in items.iter().take(5) {
        println!("  {} ${:.2}", name, cost);
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    fn make_product(id: &str, price: f64, stock: u32) -> Product {
        Product { id:id.into(), name:id.into(), price:Price{amount:price, currency:Currency::USD}, stock }
    }
    #[test] fn order_total_with_discount() {
        let p = make_product("P1", 10.0, 5);
        let o = Order { id:"T".into(), items:vec![OrderItem{product:p,qty:2}], discount:0.1 };
        assert!((order_total(&o) - 18.0).abs() < 1e-10);
    }
    #[test] fn validate_empty() {
        let o = Order{id:"".into(),items:vec![],discount:0.0};
        assert!(matches!(validate_order(&o), Err(OrderError::EmptyOrder)));
    }
    #[test] fn validate_bad_discount() {
        let p = make_product("P",1.0,10);
        let o = Order{id:"".into(),items:vec![OrderItem{product:p,qty:1}],discount:1.5};
        assert!(matches!(validate_order(&o), Err(OrderError::InvalidDiscount(_))));
    }
    #[test] fn validate_insufficient_stock() {
        let p = make_product("P",1.0,1);
        let o = Order{id:"".into(),items:vec![OrderItem{product:p,qty:5}],discount:0.0};
        assert!(matches!(validate_order(&o), Err(OrderError::InsufficientStock{..})));
    }
}
(* Grand tour of FP in OCaml *)
(* A mini order-processing system *)

type currency = USD | EUR | GBP
type price = { amount: float; currency: currency }
type product = { id: string; name: string; price: price; stock: int }
type order_item = { product: product; qty: int }
type order = { id: string; items: order_item list; discount: float }

(* Pure functions *)
let item_subtotal item = item.product.price.amount *. float_of_int item.qty
let order_total order =
  let subtotal = List.fold_left (fun acc i -> acc +. item_subtotal i) 0.0 order.items in
  subtotal *. (1.0 -. order.discount)

let validate_order order =
  let check_stock item =
    if item.qty <= item.product.stock then Ok item
    else Error (Printf.sprintf "%s: only %d in stock" item.product.name item.product.stock)
  in
  let rec validate = function
    | []      -> Ok []
    | x :: xs ->
      match (check_stock x, validate xs) with
      | (Ok i, Ok rest) -> Ok (i :: rest)
      | (Error e, _) | (_, Error e) -> Error e
  in
  match validate order.items with
  | Error e -> Error e
  | Ok _    -> if order.discount < 0.0 || order.discount > 1.0
               then Error "invalid discount" else Ok order

let () =
  let p1 = { id="A"; name="Widget"; price={amount=9.99;currency=USD}; stock=10 } in
  let p2 = { id="B"; name="Gadget"; price={amount=24.99;currency=USD}; stock=2 } in
  let order = { id="ORD-001"; discount=0.1;
    items=[{product=p1;qty=2};{product=p2;qty=1}] } in
  (match validate_order order with
  | Ok o   -> Printf.printf "Order total: $%.2f\n" (order_total o)
  | Error e -> Printf.printf "Error: %s\n" e)