ExamplesBy LevelBy TopicLearning Paths
592 Fundamental

Command Pattern (Functional Style)

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "Command Pattern (Functional Style)" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Functional Programming. The OOP command pattern encapsulates actions as objects with `execute` and `undo` methods. Key difference from OCaml: 1. **Enum vs class hierarchy**: Rust enum variants are the natural command representation; OCaml uses variant types identically; OOP would use `interface Command` with subclasses.

Tutorial

The Problem

The OOP command pattern encapsulates actions as objects with execute and undo methods. The functional version is simpler: commands are data (enum variants), interpretation is a separate function. This separation — data from behavior — makes commands serializable, loggable, testable, and replayable without coupling them to execution context. It is the foundation of event sourcing, undo/redo systems, game replay, and distributed systems that need to log and replay operations.

🎯 Learning Outcomes

  • • How commands as enum variants separate data from execution
  • • How a DrawState interpreter executes commands against mutable state
  • • How storing Vec<DrawCmd> enables replay, undo, and serialization
  • • How functional commands differ from OOP commands (no dyn Trait needed)
  • • Where functional command pattern appears: event sourcing, game replay, undo history, CQRS
  • Code Example

    enum DrawCmd {
        MoveTo(f64, f64),
        LineTo(f64, f64),
        SetColor(String),
    }

    Key Differences

  • Enum vs class hierarchy: Rust enum variants are the natural command representation; OCaml uses variant types identically; OOP would use interface Command with subclasses.
  • Interpreter as function: Both languages use a plain function for interpretation — no dyn Trait indirection or virtual dispatch needed for the simple case.
  • Serialization: Rust commands with #[derive(serde::Serialize)] become JSON-serializable; OCaml uses ppx_serde or manual serialization.
  • Undo: Both languages handle undo by storing inverse commands or snapshots — the data-oriented approach makes this straightforward.
  • OCaml Approach

    type draw_cmd = MoveTo of float * float | LineTo of float * float | SetColor of string
    type state = { mutable x: float; mutable y: float; mutable color: string }
    let interpret state = function
      | MoveTo (x, y) -> state.x <- x; state.y <- y
      | LineTo (x, y) -> (* draw line from state.{x,y} to x,y *) state.x <- x; state.y <- y
      | SetColor c -> state.color <- c
    let replay state cmds = List.iter (interpret state) cmds
    

    Full Source

    #![allow(clippy::all)]
    //! # Command Pattern (Functional Style)
    //!
    //! Commands as data structures that can be stored, replayed, and undone.
    
    /// Drawing commands for a simple graphics system.
    #[derive(Debug, Clone, PartialEq)]
    pub enum DrawCmd {
        MoveTo(f64, f64),
        LineTo(f64, f64),
        ArcTo(f64, f64, f64), // x, y, radius
        SetColor(String),
        SetWidth(f64),
        Fill,
        Stroke,
    }
    
    /// State of the drawing context.
    #[derive(Debug, Clone, Default)]
    pub struct DrawState {
        pub x: f64,
        pub y: f64,
        pub color: String,
        pub width: f64,
    }
    
    impl DrawState {
        pub fn new() -> Self {
            Self {
                color: "black".into(),
                width: 1.0,
                ..Default::default()
            }
        }
    }
    
    /// Execute a single command, updating state and optionally producing output.
    pub fn execute(state: &mut DrawState, cmd: &DrawCmd) -> Option<String> {
        match cmd {
            DrawCmd::MoveTo(x, y) => {
                state.x = *x;
                state.y = *y;
                None
            }
            DrawCmd::LineTo(x, y) => {
                let log = format!(
                    "line ({:.1},{:.1})->({:.1},{:.1}) color={} width={}",
                    state.x, state.y, x, y, state.color, state.width
                );
                state.x = *x;
                state.y = *y;
                Some(log)
            }
            DrawCmd::ArcTo(x, y, r) => {
                let log = format!(
                    "arc ({:.1},{:.1})->({:.1},{:.1}) r={:.1}",
                    state.x, state.y, x, y, r
                );
                state.x = *x;
                state.y = *y;
                Some(log)
            }
            DrawCmd::SetColor(c) => {
                state.color = c.clone();
                None
            }
            DrawCmd::SetWidth(w) => {
                state.width = *w;
                None
            }
            DrawCmd::Fill => Some(format!("fill with {}", state.color)),
            DrawCmd::Stroke => Some(format!("stroke with {} width={}", state.color, state.width)),
        }
    }
    
    /// Build a rectangle as a sequence of commands.
    pub fn rect(x: f64, y: f64, w: f64, h: f64) -> Vec<DrawCmd> {
        vec![
            DrawCmd::MoveTo(x, y),
            DrawCmd::LineTo(x + w, y),
            DrawCmd::LineTo(x + w, y + h),
            DrawCmd::LineTo(x, y + h),
            DrawCmd::LineTo(x, y),
        ]
    }
    
    /// Build a circle approximation.
    pub fn circle(cx: f64, cy: f64, r: f64) -> Vec<DrawCmd> {
        vec![
            DrawCmd::MoveTo(cx + r, cy),
            DrawCmd::ArcTo(cx, cy + r, r),
            DrawCmd::ArcTo(cx - r, cy, r),
            DrawCmd::ArcTo(cx, cy - r, r),
            DrawCmd::ArcTo(cx + r, cy, r),
        ]
    }
    
    /// Replay a sequence of commands.
    pub fn replay(cmds: &[DrawCmd]) -> (DrawState, Vec<String>) {
        let mut state = DrawState::new();
        let mut log = Vec::new();
        for cmd in cmds {
            if let Some(entry) = execute(&mut state, cmd) {
                log.push(entry);
            }
        }
        (state, log)
    }
    
    /// Pure command application (returns new state, no mutation).
    pub fn apply(state: DrawState, cmd: &DrawCmd) -> DrawState {
        let mut new_state = state;
        execute(&mut new_state, cmd);
        new_state
    }
    
    /// Count different command types.
    pub fn count_commands(cmds: &[DrawCmd]) -> (usize, usize, usize) {
        let moves = cmds
            .iter()
            .filter(|c| matches!(c, DrawCmd::MoveTo(_, _)))
            .count();
        let lines = cmds
            .iter()
            .filter(|c| matches!(c, DrawCmd::LineTo(_, _)))
            .count();
        let style = cmds
            .iter()
            .filter(|c| matches!(c, DrawCmd::SetColor(_) | DrawCmd::SetWidth(_)))
            .count();
        (moves, lines, style)
    }
    
    /// Optimize commands by removing redundant style changes.
    pub fn optimize(cmds: Vec<DrawCmd>) -> Vec<DrawCmd> {
        let mut result = Vec::new();
        let mut last_color: Option<String> = None;
        let mut last_width: Option<f64> = None;
    
        for cmd in cmds {
            match &cmd {
                DrawCmd::SetColor(c) if last_color.as_ref() == Some(c) => continue,
                DrawCmd::SetWidth(w) if last_width == Some(*w) => continue,
                DrawCmd::SetColor(c) => last_color = Some(c.clone()),
                DrawCmd::SetWidth(w) => last_width = Some(*w),
                _ => {}
            }
            result.push(cmd);
        }
        result
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_move_to() {
            let mut state = DrawState::new();
            execute(&mut state, &DrawCmd::MoveTo(10.0, 20.0));
            assert_eq!((state.x, state.y), (10.0, 20.0));
        }
    
        #[test]
        fn test_line_to() {
            let mut state = DrawState::new();
            state.x = 5.0;
            state.y = 5.0;
            let log = execute(&mut state, &DrawCmd::LineTo(10.0, 10.0));
            assert!(log.is_some());
            assert_eq!((state.x, state.y), (10.0, 10.0));
        }
    
        #[test]
        fn test_rect_commands() {
            let cmds = rect(0.0, 0.0, 10.0, 10.0);
            assert_eq!(cmds.len(), 5);
        }
    
        #[test]
        fn test_replay() {
            let cmds = vec![
                DrawCmd::SetColor("red".into()),
                DrawCmd::MoveTo(0.0, 0.0),
                DrawCmd::LineTo(10.0, 0.0),
                DrawCmd::LineTo(10.0, 10.0),
            ];
            let (state, log) = replay(&cmds);
            assert_eq!(state.color, "red");
            assert_eq!(log.len(), 2); // Two LineTo commands produce output
        }
    
        #[test]
        fn test_pure_apply() {
            let state = DrawState::new();
            let state2 = apply(state.clone(), &DrawCmd::MoveTo(5.0, 5.0));
            assert_eq!(state.x, 0.0); // Original unchanged
            assert_eq!(state2.x, 5.0);
        }
    
        #[test]
        fn test_count_commands() {
            let cmds = vec![
                DrawCmd::MoveTo(0.0, 0.0),
                DrawCmd::LineTo(1.0, 1.0),
                DrawCmd::LineTo(2.0, 2.0),
                DrawCmd::SetColor("red".into()),
            ];
            let (moves, lines, style) = count_commands(&cmds);
            assert_eq!(moves, 1);
            assert_eq!(lines, 2);
            assert_eq!(style, 1);
        }
    
        #[test]
        fn test_optimize_removes_redundant() {
            let cmds = vec![
                DrawCmd::SetColor("red".into()),
                DrawCmd::SetColor("red".into()), // Redundant
                DrawCmd::LineTo(1.0, 1.0),
                DrawCmd::SetColor("blue".into()),
            ];
            let optimized = optimize(cmds);
            assert_eq!(optimized.len(), 3);
        }
    
        #[test]
        fn test_circle() {
            let cmds = circle(0.0, 0.0, 5.0);
            assert_eq!(cmds.len(), 5); // MoveTo + 4 ArcTo
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_move_to() {
            let mut state = DrawState::new();
            execute(&mut state, &DrawCmd::MoveTo(10.0, 20.0));
            assert_eq!((state.x, state.y), (10.0, 20.0));
        }
    
        #[test]
        fn test_line_to() {
            let mut state = DrawState::new();
            state.x = 5.0;
            state.y = 5.0;
            let log = execute(&mut state, &DrawCmd::LineTo(10.0, 10.0));
            assert!(log.is_some());
            assert_eq!((state.x, state.y), (10.0, 10.0));
        }
    
        #[test]
        fn test_rect_commands() {
            let cmds = rect(0.0, 0.0, 10.0, 10.0);
            assert_eq!(cmds.len(), 5);
        }
    
        #[test]
        fn test_replay() {
            let cmds = vec![
                DrawCmd::SetColor("red".into()),
                DrawCmd::MoveTo(0.0, 0.0),
                DrawCmd::LineTo(10.0, 0.0),
                DrawCmd::LineTo(10.0, 10.0),
            ];
            let (state, log) = replay(&cmds);
            assert_eq!(state.color, "red");
            assert_eq!(log.len(), 2); // Two LineTo commands produce output
        }
    
        #[test]
        fn test_pure_apply() {
            let state = DrawState::new();
            let state2 = apply(state.clone(), &DrawCmd::MoveTo(5.0, 5.0));
            assert_eq!(state.x, 0.0); // Original unchanged
            assert_eq!(state2.x, 5.0);
        }
    
        #[test]
        fn test_count_commands() {
            let cmds = vec![
                DrawCmd::MoveTo(0.0, 0.0),
                DrawCmd::LineTo(1.0, 1.0),
                DrawCmd::LineTo(2.0, 2.0),
                DrawCmd::SetColor("red".into()),
            ];
            let (moves, lines, style) = count_commands(&cmds);
            assert_eq!(moves, 1);
            assert_eq!(lines, 2);
            assert_eq!(style, 1);
        }
    
        #[test]
        fn test_optimize_removes_redundant() {
            let cmds = vec![
                DrawCmd::SetColor("red".into()),
                DrawCmd::SetColor("red".into()), // Redundant
                DrawCmd::LineTo(1.0, 1.0),
                DrawCmd::SetColor("blue".into()),
            ];
            let optimized = optimize(cmds);
            assert_eq!(optimized.len(), 3);
        }
    
        #[test]
        fn test_circle() {
            let cmds = circle(0.0, 0.0, 5.0);
            assert_eq!(cmds.len(), 5); // MoveTo + 4 ArcTo
        }
    }

    Deep Comparison

    OCaml vs Rust: Command Pattern (Functional)

    Command as Data

    OCaml

    type cmd =
      | MoveTo  of float * float
      | LineTo  of float * float
      | SetColor of string
    

    Rust

    enum DrawCmd {
        MoveTo(f64, f64),
        LineTo(f64, f64),
        SetColor(String),
    }
    

    Command Execution

    OCaml

    let execute state cmd =
      match cmd with
      | MoveTo(x, y) -> state.x <- x; state.y <- y
      | LineTo(x, y) -> 
          Printf.printf "line -> (%f,%f)\n" x y;
          state.x <- x; state.y <- y
      | SetColor c -> state.color <- c
    

    Rust

    fn execute(state: &mut DrawState, cmd: &DrawCmd) -> Option<String> {
        match cmd {
            DrawCmd::MoveTo(x, y) => {
                state.x = *x; state.y = *y;
                None
            }
            DrawCmd::LineTo(x, y) => {
                let log = format!("line -> ({},{})", x, y);
                state.x = *x; state.y = *y;
                Some(log)
            }
            DrawCmd::SetColor(c) => {
                state.color = c.clone();
                None
            }
        }
    }
    

    Command Composition

    Both languages can compose commands into sequences:

    fn rect(x: f64, y: f64, w: f64, h: f64) -> Vec<DrawCmd> {
        vec![
            DrawCmd::MoveTo(x, y),
            DrawCmd::LineTo(x + w, y),
            DrawCmd::LineTo(x + w, y + h),
            DrawCmd::LineTo(x, y + h),
            DrawCmd::LineTo(x, y),
        ]
    }
    

    Benefits

  • Serializable - Commands can be saved/transmitted
  • Replayable - Reconstruct state from command history
  • Optimizable - Remove redundant commands
  • Testable - Test command sequences in isolation
  • Exercises

  • Undo stack: Add an undo_stack: Vec<DrawCmd> to DrawState that stores inverse commands; implement undo(state: &mut DrawState) that pops and executes the inverse.
  • Command filter: Write fn remove_color_changes(cmds: Vec<DrawCmd>) -> Vec<DrawCmd> that removes all SetColor commands from a command list.
  • Command serialization: Add #[derive(serde::Serialize, serde::Deserialize)] to DrawCmd and write functions to save/load a command sequence to/from JSON.
  • Open Source Repos