๐Ÿฆ€ Functional Rust

059: Expression Tree

Difficulty: 2 Level: Beginner Represent arithmetic expressions as a recursive enum and evaluate them with structural recursion.

The Problem This Solves

Calculators, compilers, query engines, and rule systems all need to represent expressions: things that can be composed from smaller expressions. `(1 + 2) * (10 - 4)` is an expression. Its sub-expressions (`1 + 2`) and (`10 - 4`) are expressions too, composed of leaf values. A flat `String` or `Vec` can't capture this structure. You need a recursive data type: an expression is either a leaf (a number) or a node (an operator applied to two sub-expressions). This is an Abstract Syntax Tree โ€” the core data structure of every compiler. Without a proper recursive type, you end up with string parsing, switch statements on magic strings, or parallel arrays โ€” all fragile. The enum makes the structure explicit and the compiler enforces it.

The Intuition

An expression tree is a tree where: Evaluating the tree is structural recursion: to evaluate `Add(l, r)`, evaluate `l`, evaluate `r`, add the results. The structure of the code mirrors the structure of the data.

How It Works in Rust

#[derive(Debug, Clone, PartialEq)]
pub enum Expr {
 Num(f64),
 Add(Box<Expr>, Box<Expr>),  // Box<Expr> required: Expr cannot have infinite size
 Sub(Box<Expr>, Box<Expr>),
 Mul(Box<Expr>, Box<Expr>),
 Div(Box<Expr>, Box<Expr>),
}

impl Expr {
 // Convenience constructors โ€” hide Box::new boilerplate
 pub fn num(n: f64) -> Self { Expr::Num(n) }
 pub fn new_add(l: Expr, r: Expr) -> Self { Expr::Add(Box::new(l), Box::new(r)) }

 // Structural recursion: eval mirrors the shape of the type
 pub fn eval(&self) -> f64 {
     match self {
         Expr::Num(n)      => *n,
         Expr::Add(l, r)   => l.eval() + r.eval(),
         Expr::Sub(l, r)   => l.eval() - r.eval(),
         Expr::Mul(l, r)   => l.eval() * r.eval(),
         Expr::Div(l, r)   => l.eval() / r.eval(),
     }
 }
}

// Build: (1 + 2) * (10 - 4) = 18
let e = Expr::new_mul(
 Expr::new_add(Expr::num(1.0), Expr::num(2.0)),
 Expr::new_sub(Expr::num(10.0), Expr::num(4.0)),
);
assert!((e.eval() - 18.0).abs() < f64::EPSILON);
`Box<Expr>` provides one level of heap indirection, giving `Expr` a known size at compile time. Without it, `Expr` would contain `Expr` which would contain `Expr` โ€” infinite size.

What This Unlocks

Key Differences

ConceptOCamlRust
Recursive typeImplicit heap allocation`Box<Expr>` required for known size
Constructors`Add (Num 1.0, Num 2.0)``Expr::new_add(Expr::num(1.0), Expr::num(2.0))`
Eval function`let rec eval = function ...``impl Expr { fn eval(&self) -> f64 }`
DisplayManual `to_string` function`impl Display for Expr` โ€” enables `format!("{e}")`
Division safetyReturns `infinity` silentlyCan return `Result` for explicit error
//! # Recursive Variant โ€” Expression Tree
//!
//! OCaml's recursive variants with payloads map to Rust's `enum` with
//! `Box`-wrapped recursive fields. The key difference: Rust requires
//! explicit heap allocation (`Box`) for recursive types because it must
//! know the size of every type at compile time.

// ---------------------------------------------------------------------------
// Approach A: Idiomatic Rust โ€” enum with Box, methods via impl
// ---------------------------------------------------------------------------

/// An arithmetic expression tree.
///
/// `Box<Expr>` is required because `Expr` is recursive โ€” without it,
/// `Expr` would have infinite size. OCaml handles this implicitly because
/// all variant payloads are heap-allocated.
#[derive(Debug, Clone, PartialEq)]
pub enum Expr {
    Num(f64),
    Add(Box<Expr>, Box<Expr>),
    Sub(Box<Expr>, Box<Expr>),
    Mul(Box<Expr>, Box<Expr>),
    Div(Box<Expr>, Box<Expr>),
}

/// Convenience constructors to avoid writing `Box::new(...)` everywhere.
/// This is a common Rust pattern for recursive data structures.
impl Expr {
    pub fn num(n: f64) -> Self {
        Expr::Num(n)
    }

    pub fn new_add(l: Expr, r: Expr) -> Self {
        Expr::Add(Box::new(l), Box::new(r))
    }

    pub fn new_sub(l: Expr, r: Expr) -> Self {
        Expr::Sub(Box::new(l), Box::new(r))
    }

    pub fn new_mul(l: Expr, r: Expr) -> Self {
        Expr::Mul(Box::new(l), Box::new(r))
    }

    pub fn new_div(l: Expr, r: Expr) -> Self {
        Expr::Div(Box::new(l), Box::new(r))
    }
}

impl Expr {
    /// Evaluate the expression tree recursively.
    ///
    /// Mirrors OCaml's `eval` function โ€” structural recursion over the variant.
    /// Takes `&self` (borrowed reference) rather than consuming the tree.
    pub fn eval(&self) -> f64 {
        match self {
            Expr::Num(n) => *n,
            Expr::Add(l, r) => l.eval() + r.eval(),
            Expr::Sub(l, r) => l.eval() - r.eval(),
            Expr::Mul(l, r) => l.eval() * r.eval(),
            Expr::Div(l, r) => l.eval() / r.eval(),
        }
    }
}

/// Display produces the same parenthesized format as OCaml's `to_string`.
impl std::fmt::Display for Expr {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Expr::Num(n) => write!(f, "{n}"),
            Expr::Add(l, r) => write!(f, "({l} + {r})"),
            Expr::Sub(l, r) => write!(f, "({l} - {r})"),
            Expr::Mul(l, r) => write!(f, "({l} * {r})"),
            Expr::Div(l, r) => write!(f, "({l} / {r})"),
        }
    }
}

// ---------------------------------------------------------------------------
// Approach B: Free functions โ€” closer to OCaml style
// ---------------------------------------------------------------------------

/// Evaluate as a standalone function (OCaml style).
pub fn eval(expr: &Expr) -> f64 {
    expr.eval()
}

/// Convert to string as a standalone function.
pub fn to_string(expr: &Expr) -> String {
    format!("{expr}")
}

// ---------------------------------------------------------------------------
// Approach C: Safe division with Result
// ---------------------------------------------------------------------------

/// Division-safe evaluation that returns an error on divide-by-zero
/// instead of producing `inf` or `NaN`.
///
/// This is a Rust improvement over the OCaml version โ€” OCaml's version
/// silently produces `infinity` on division by zero.
pub fn eval_safe(expr: &Expr) -> Result<f64, String> {
    match expr {
        Expr::Num(n) => Ok(*n),
        Expr::Add(l, r) => Ok(eval_safe(l)? + eval_safe(r)?),
        Expr::Sub(l, r) => Ok(eval_safe(l)? - eval_safe(r)?),
        Expr::Mul(l, r) => Ok(eval_safe(l)? * eval_safe(r)?),
        Expr::Div(l, r) => {
            let divisor = eval_safe(r)?;
            if divisor == 0.0 {
                Err(format!("Division by zero: {expr}"))
            } else {
                Ok(eval_safe(l)? / divisor)
            }
        }
    }
}

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

    // (1 + 2) * (10 - 4) = 18
    fn sample_expr() -> Expr {
        Expr::new_mul(
            Expr::new_add(Expr::num(1.0), Expr::num(2.0)),
            Expr::new_sub(Expr::num(10.0), Expr::num(4.0)),
        )
    }

    #[test]
    fn test_eval_basic() {
        assert!((sample_expr().eval() - 18.0).abs() < f64::EPSILON);
    }

    #[test]
    fn test_eval_single_num() {
        assert!((Expr::num(42.0).eval() - 42.0).abs() < f64::EPSILON);
    }

    #[test]
    fn test_display() {
        assert_eq!(to_string(&sample_expr()), "((1 + 2) * (10 - 4))");
    }

    #[test]
    fn test_eval_nested_division() {
        // 10 / (5 - 3) = 5
        let e = Expr::new_div(
            Expr::num(10.0),
            Expr::new_sub(Expr::num(5.0), Expr::num(3.0)),
        );
        assert!((e.eval() - 5.0).abs() < f64::EPSILON);
    }

    #[test]
    fn test_eval_safe_division_by_zero() {
        let e = Expr::new_div(Expr::num(1.0), Expr::num(0.0));
        assert!(eval_safe(&e).is_err());
    }

    #[test]
    fn test_eval_safe_ok() {
        assert!((eval_safe(&sample_expr()).unwrap() - 18.0).abs() < f64::EPSILON);
    }

    #[test]
    fn test_display_num() {
        assert_eq!(format!("{}", Expr::num(3.14)), "3.14");
    }

    #[test]
    fn test_clone_and_eq() {
        let e = sample_expr();
        let e2 = e.clone();
        assert_eq!(e, e2);
    }
}

fn main() {
    println!("{:?}", (sample_expr().eval() - 18.0).abs() < f64::EPSILON);
    println!("{:?}", (Expr::num(42.0).eval() - 42.0).abs() < f64::EPSILON);
    println!("{:?}", to_string(&sample_expr()), "((1 + 2) * (10 - 4))");
}
(* Recursive Variant โ€” Expression Tree *)

type expr =
  | Num of float
  | Add of expr * expr
  | Sub of expr * expr
  | Mul of expr * expr
  | Div of expr * expr

(* Implementation 1: Direct recursive eval *)
let rec eval = function
  | Num n         -> n
  | Add (l, r)    -> eval l +. eval r
  | Sub (l, r)    -> eval l -. eval r
  | Mul (l, r)    -> eval l *. eval r
  | Div (l, r)    -> eval l /. eval r

(* Implementation 2: to_string with parentheses *)
let rec to_string = function
  | Num n         -> string_of_float n
  | Add (l, r)    -> Printf.sprintf "(%s + %s)" (to_string l) (to_string r)
  | Sub (l, r)    -> Printf.sprintf "(%s - %s)" (to_string l) (to_string r)
  | Mul (l, r)    -> Printf.sprintf "(%s * %s)" (to_string l) (to_string r)
  | Div (l, r)    -> Printf.sprintf "(%s / %s)" (to_string l) (to_string r)

(* Tests *)
let () =
  (* (1 + 2) * (10 - 4) = 18 *)
  let e = Mul (Add (Num 1., Num 2.), Sub (Num 10., Num 4.)) in
  assert (eval e = 18.);
  assert (eval (Num 42.) = 42.);
  assert (eval (Div (Num 10., Sub (Num 5., Num 3.))) = 5.);
  Printf.printf "%s = %.1f\n" (to_string e) (eval e);
  Printf.printf "All expression-tree tests passed!\n"

๐Ÿ“Š Detailed Comparison

Comparison: Expression Tree โ€” OCaml vs Rust

OCaml

๐Ÿช Show OCaml equivalent
type expr =
| Num of float
| Add of expr * expr
| Mul of expr * expr  (* etc. *)

let rec eval = function
| Num n      -> n
| Add (l, r) -> eval l +. eval r
| Mul (l, r) -> eval l *. eval r
(* ... *)

let rec to_string = function
| Num n      -> string_of_float n
| Add (l, r) -> Printf.sprintf "(%s + %s)" (to_string l) (to_string r)
(* ... *)

Rust โ€” Idiomatic (Box + impl)

#[derive(Debug, Clone, PartialEq)]
pub enum Expr {
 Num(f64),
 Add(Box<Expr>, Box<Expr>),
 Mul(Box<Expr>, Box<Expr>),  // etc.
}

impl Expr {
 pub fn new_add(l: Expr, r: Expr) -> Self {
     Expr::Add(Box::new(l), Box::new(r))
 }

 pub fn eval(&self) -> f64 {
     match self {
         Expr::Num(n)      => *n,
         Expr::Add(l, r)   => l.eval() + r.eval(),
         Expr::Mul(l, r)   => l.eval() * r.eval(),
         // ...
     }
 }
}

Rust โ€” Safe Division (Result)

pub fn eval_safe(expr: &Expr) -> Result<f64, String> {
 match expr {
     Expr::Div(l, r) => {
         let divisor = eval_safe(r)?;
         if divisor == 0.0 { Err("Division by zero".into()) }
         else { Ok(eval_safe(l)? / divisor) }
     }
     // ...
 }
}

Comparison Table

AspectOCamlRust
Recursive type`type expr = Num of float \Add of expr expr``enum Expr { Num(f64), Add(Box<Expr>, Box<Expr>) }`
Heap allocationImplicit (GC-managed)Explicit with `Box<T>`
Constructor`Add (l, r)` directly`Expr::Add(Box::new(l), Box::new(r))` or helper
Pattern matching`\Add (l, r) -> ...``Expr::Add(l, r) => ...` (l, r are `&Box<Expr>`)
Float ops`+.` `-` `.` `/.` (separate from int)`+` `-` `` `/` (overloaded via traits)
Division safetyReturns `infinity`Can return `Result` with error
Pretty-printCustom `to_string` function`impl Display` trait

Type Signatures Explained

OCaml: `val eval : expr -> float` โ€” takes an expression, returns a float. The recursive structure is handled by the GC.

Rust: `fn eval(&self) -> f64` โ€” borrows `self` (`&Expr`). When matching `Add(l, r)`, `l` and `r` are `&Box<Expr>`, which auto-derefs to `&Expr` for the recursive call.

Takeaways

1. Box is the price of ownership: OCaml's GC handles recursive types transparently; Rust makes heap allocation explicit 2. Constructor boilerplate: Helper functions like `new_add()` are a common Rust pattern to hide `Box::new` 3. Safety upgrade: Rust's `Result` type makes error handling for division explicit and composable with `?` 4. Display trait integrates with `format!`, `println!`, and `to_string()` automatically 5. Auto-deref makes working with `Box<Expr>` ergonomic โ€” you rarely notice the indirection in pattern matching