๐Ÿฆ€ Functional Rust
๐ŸŽฌ How Rust Iterators Work Lazy evaluation, chaining, collect(), and zero-cost abstractions.
๐Ÿ“ Text version (for readers / accessibility)

โ€ข Iterators are lazy โ€” .map(), .filter(), .take() build a chain but do no work until consumed

โ€ข .collect() triggers evaluation, transforming the chain into a Vec, HashMap, or other collection

โ€ข Zero-cost abstraction: iterator chains compile to the same machine code as hand-written loops

โ€ข .iter() borrows, .into_iter() consumes, .iter_mut() borrows mutably

โ€ข Chaining replaces nested loops with a readable, composable pipeline

096: Recursive Descent Parser

Difficulty: 3 Level: Intermediate Parse arithmetic expressions into an AST using hand-written recursive descent โ€” no parser libraries needed.

The Problem This Solves

Every language, config format, and query engine needs a parser. You could reach for a parser generator (nom, pest, lalrpop), but understanding recursive descent first makes you a better user of those tools โ€” and sometimes a hand-written parser is the right choice for simple grammars. Recursive descent is the most readable parsing technique: the grammar rules map directly to functions. One function per grammar rule, functions call each other, and operator precedence falls out naturally from the call hierarchy. The key insight: `*` binds tighter than `+` because `parse_expr` calls `parse_term`, not the other way around. Higher precedence = deeper in the call stack.

The Intuition

The grammar `expr = term ('+' expr)?` translates almost word-for-word into code:
parse_expr โ†’ call parse_term, then check for '+', then call parse_expr again
parse_term โ†’ call parse_atom, then check for '*', then call parse_term again
parse_atom โ†’ parse a number
Each function consumes a prefix of the token list and returns `(parsed_node, remaining_tokens)`. The slice `split_first()` method mirrors OCaml's list head/tail pattern match.

How It Works in Rust

#[derive(Debug)]
pub enum Expr {
 Num(i64),
 Add(Box<Expr>, Box<Expr>),  // Box for heap allocation โ€” recursive type
 Mul(Box<Expr>, Box<Expr>),
}

// parse_expr handles lowest precedence (+)
fn parse_expr<'a>(tokens: &'a [&str]) -> Result<(Expr, &'a [&str]), String> {
 let (left, rest) = parse_term(tokens)?;  // parse higher-precedence first
 if let Some(("+", rest)) = rest.split_first() {
     let (right, rest) = parse_expr(rest)?;  // right-recursive for +
     Ok((Expr::Add(Box::new(left), Box::new(right)), rest))
 } else {
     Ok((left, rest))
 }
}

// parse_term handles * (tighter than +)
fn parse_term<'a>(tokens: &'a [&str]) -> Result<(Expr, &'a [&str]), String> {
 let (left, rest) = parse_atom(tokens)?;
 if let Some(("*", rest)) = rest.split_first() {
     let (right, rest) = parse_term(rest)?;
     Ok((Expr::Mul(Box::new(left), Box::new(right)), rest))
 } else {
     Ok((left, rest))
 }
}

// parse_atom handles numbers (highest precedence โ€” leaves of the AST)
fn parse_atom<'a>(tokens: &'a [&str]) -> Result<(Expr, &'a [&str]), String> {
 match tokens.split_first() {
     Some((token, rest)) => Ok((Expr::Num(token.parse()?), rest)),
     None => Err("unexpected end of input".into()),
 }
}
Evaluate the AST recursively:
pub fn eval(expr: &Expr) -> i64 {
 match expr {
     Expr::Num(n) => *n,
     Expr::Add(a, b) => eval(a) + eval(b),
     Expr::Mul(a, b) => eval(a) * eval(b),
 }
}
`"2" + "3" "4"` โ†’ `2 + (3 4)` = 14. Precedence is correct by construction.

What This Unlocks

Key Differences

ConceptOCamlRust
Mutual recursion`let rec ... and ...` keywordFunctions simply call each other
Recursive typesImplicit heap allocation`Box<Expr>` โ€” explicit heap
Token consumptionPattern match on list head`split_first()` on slice
Error handling`raise` / `failwith``Result<T, String>`, `?` operator
LifetimeNot needed`'a` on slice refs โ€” ties output to input
//! # Simple Recursive Descent Parser
//!
//! Parse arithmetic expressions into an AST with correct precedence.
//! OCaml's mutual recursion with `and` maps to Rust functions calling each other.

#[derive(Debug, PartialEq)]
pub enum Expr {
    Num(i64),
    Add(Box<Expr>, Box<Expr>),
    Mul(Box<Expr>, Box<Expr>),
}

// ---------------------------------------------------------------------------
// Approach A: Slice-based parser (mirrors OCaml's list consumption)
// ---------------------------------------------------------------------------

pub fn parse<'a>(tokens: &'a [&'a str]) -> Result<Expr, String> {
    let (expr, rest) = parse_expr(tokens)?;
    if rest.is_empty() {
        Ok(expr)
    } else {
        Err(format!("unexpected tokens: {:?}", rest))
    }
}

fn parse_expr<'a>(tokens: &'a [&'a str]) -> Result<(Expr, &'a [&'a str]), String> {
    let (left, rest) = parse_term(tokens)?;
    if let Some((&"+", rest)) = rest.split_first() {
        let (right, rest) = parse_expr(rest)?;
        Ok((Expr::Add(Box::new(left), Box::new(right)), rest))
    } else {
        Ok((left, rest))
    }
}

fn parse_term<'a>(tokens: &'a [&'a str]) -> Result<(Expr, &'a [&'a str]), String> {
    let (left, rest) = parse_atom(tokens)?;
    if let Some((&"*", rest)) = rest.split_first() {
        let (right, rest) = parse_term(rest)?;
        Ok((Expr::Mul(Box::new(left), Box::new(right)), rest))
    } else {
        Ok((left, rest))
    }
}

fn parse_atom<'a>(tokens: &'a [&'a str]) -> Result<(Expr, &'a [&'a str]), String> {
    match tokens.split_first() {
        Some((token, rest)) => {
            let n: i64 = token.parse().map_err(|_| format!("not a number: {}", token))?;
            Ok((Expr::Num(n), rest))
        }
        None => Err("unexpected end of input".to_string()),
    }
}

// ---------------------------------------------------------------------------
// Approach B: Evaluator (mirrors OCaml's eval)
// ---------------------------------------------------------------------------

pub fn eval(expr: &Expr) -> i64 {
    match expr {
        Expr::Num(n) => *n,
        Expr::Add(a, b) => eval(a) + eval(b),
        Expr::Mul(a, b) => eval(a) * eval(b),
    }
}

// ---------------------------------------------------------------------------
// Approach C: Index-based parser (avoids slice lifetimes)
// ---------------------------------------------------------------------------

pub fn parse_and_eval(tokens: &[&str]) -> Result<i64, String> {
    let expr = parse(tokens)?;
    Ok(eval(&expr))
}

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

    #[test]
    fn test_simple_add() {
        assert_eq!(parse_and_eval(&["2", "+", "3"]), Ok(5));
    }

    #[test]
    fn test_simple_mul() {
        assert_eq!(parse_and_eval(&["2", "*", "3"]), Ok(6));
    }

    #[test]
    fn test_precedence() {
        assert_eq!(parse_and_eval(&["2", "+", "3", "*", "4"]), Ok(14));
    }

    #[test]
    fn test_single_number() {
        assert_eq!(parse_and_eval(&["42"]), Ok(42));
    }

    #[test]
    fn test_empty() {
        assert!(parse_and_eval(&[]).is_err());
    }

    #[test]
    fn test_complex() {
        assert_eq!(parse_and_eval(&["1", "+", "2", "+", "3"]), Ok(6));
    }
}
type expr = Num of int | Add of expr * expr | Mul of expr * expr

let rec parse_expr tokens = 
  let left, rest = parse_term tokens in
  match rest with
  | "+" :: rest' ->
    let right, rest'' = parse_expr rest' in
    (Add (left, right), rest'')
  | _ -> (left, rest)
and parse_term tokens =
  let left, rest = parse_atom tokens in
  match rest with
  | "*" :: rest' ->
    let right, rest'' = parse_term rest' in
    (Mul (left, right), rest'')
  | _ -> (left, rest)
and parse_atom = function
  | n :: rest -> (Num (int_of_string n), rest)
  | [] -> failwith "unexpected end"

let rec eval = function
  | Num n -> n | Add (a,b) -> eval a + eval b | Mul (a,b) -> eval a * eval b

let () =
  let tokens = ["2";"+";"3";"*";"4"] in
  let ast, _ = parse_expr tokens in
  Printf.printf "2+3*4 = %d\n" (eval ast)

๐Ÿ“Š Detailed Comparison

Comparison: Recursive Descent Parser โ€” OCaml vs Rust

Core Insight

The parser structure is nearly identical: `parse_expr` calls `parse_term` calls `parse_atom`, with each level consuming tokens and returning (AST, remaining_tokens). The key Rust difference is `Box<Expr>` โ€” recursive enum variants must be heap-allocated because Rust needs compile-time size. OCaml's GC handles this transparently.

OCaml

๐Ÿช Show OCaml equivalent
type expr = Num of int | Add of expr * expr | Mul of expr * expr

let rec parse_expr tokens = 
let left, rest = parse_term tokens in
match rest with
| "+" :: rest' -> let right, rest'' = parse_expr rest' in (Add (left, right), rest'')
| _ -> (left, rest)
and parse_term tokens =
let left, rest = parse_atom tokens in
match rest with
| "*" :: rest' -> let right, rest'' = parse_term rest' in (Mul (left, right), rest'')
| _ -> (left, rest)

Rust

fn parse_expr<'a>(tokens: &'a [&str]) -> Result<(Expr, &'a [&str]), String> {
 let (left, rest) = parse_term(tokens)?;
 if let Some(("+", rest)) = rest.split_first() {
     let (right, rest) = parse_expr(rest)?;
     Ok((Expr::Add(Box::new(left), Box::new(right)), rest))
 } else { Ok((left, rest)) }
}

Comparison Table

AspectOCamlRust
Recursive type`expr * expr` directly`Box<Expr>` required
Mutual recursion`and` keywordFunctions call each other
Token consumption`"+" :: rest'` list match`split_first()` on slice
Error handling`failwith` exception`Result<T, String>`
Tuple return`(ast, rest)``(Expr, &[&str])` with lifetime
Parse int`int_of_string``str::parse::<i64>()`

Learner Notes

  • `Box<Expr>`: The #1 surprise for OCaml devs. Rust enums are stack-allocated, so recursive variants need indirection
  • Lifetimes in parsers: `&'a [&str]` โ€” the output slice borrows from the input, and Rust tracks this
  • `split_first()`: Returns `Option<(&T, &[T])>` โ€” Rust's equivalent of OCaml's list head/tail destructuring
  • No `and` keyword: Rust doesn't need it. Forward references work naturally for functions (not for types โ€” use `Box`)
  • `?` operator: Propagates parse errors elegantly โ€” each `?` is like OCaml's `match ... with Error -> ...`