ExamplesBy LevelBy TopicLearning Paths
083 Intermediate

083 — Robot Simulator

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "083 — Robot Simulator" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Simulate a robot on a grid that can turn left, turn right, and advance. Key difference from OCaml: | Aspect | Rust | OCaml |

Tutorial

The Problem

Simulate a robot on a grid that can turn left, turn right, and advance. The robot's state (x, y, dir) is immutable — each instruction returns a new Robot rather than mutating in place. Execute a sequence of instructions using fold, and compare with OCaml's record update syntax { r with ... }.

🎯 Learning Outcomes

  • • Use ..self struct update syntax for functional record updates in Rust
  • • Derive Copy for small state structs to enable pass-by-value semantics
  • • Represent direction rotation with match on a cyclic enum
  • • Fold a list of instructions with Iterator::fold
  • • Map Rust's ..self spread to OCaml's { r with field = value } update
  • • Understand when immutable state machines are preferable to mutable ones
  • Code Example

    #![allow(clippy::all)]
    /// Robot Simulator — State with Immutable Records
    ///
    /// Ownership: Robot is a small Copy type (all fields are Copy).
    /// Methods return new robots (functional style) rather than mutating.
    
    #[derive(Debug, Clone, Copy, PartialEq)]
    pub enum Direction {
        North,
        East,
        South,
        West,
    }
    
    #[derive(Debug, Clone, Copy, PartialEq)]
    pub struct Robot {
        pub x: i32,
        pub y: i32,
        pub dir: Direction,
    }
    
    #[derive(Debug, Clone, Copy)]
    pub enum Instruction {
        TurnLeft,
        TurnRight,
        Advance,
    }
    
    impl Direction {
        pub fn turn_right(self) -> Self {
            match self {
                Direction::North => Direction::East,
                Direction::East => Direction::South,
                Direction::South => Direction::West,
                Direction::West => Direction::North,
            }
        }
    
        pub fn turn_left(self) -> Self {
            match self {
                Direction::North => Direction::West,
                Direction::West => Direction::South,
                Direction::South => Direction::East,
                Direction::East => Direction::North,
            }
        }
    }
    
    impl Robot {
        pub fn new(x: i32, y: i32, dir: Direction) -> Self {
            Robot { x, y, dir }
        }
    
        /// Returns a new Robot — functional update (like OCaml { r with ... })
        pub fn advance(self) -> Self {
            match self.dir {
                Direction::North => Robot {
                    y: self.y + 1,
                    ..self
                },
                Direction::East => Robot {
                    x: self.x + 1,
                    ..self
                },
                Direction::South => Robot {
                    y: self.y - 1,
                    ..self
                },
                Direction::West => Robot {
                    x: self.x - 1,
                    ..self
                },
            }
        }
    
        pub fn execute(self, instr: Instruction) -> Self {
            match instr {
                Instruction::TurnLeft => Robot {
                    dir: self.dir.turn_left(),
                    ..self
                },
                Instruction::TurnRight => Robot {
                    dir: self.dir.turn_right(),
                    ..self
                },
                Instruction::Advance => self.advance(),
            }
        }
    
        /// Run a sequence of instructions — fold pattern
        pub fn run(self, instructions: &[Instruction]) -> Self {
            instructions.iter().fold(self, |r, &i| r.execute(i))
        }
    }
    
    /// Version 2: Parse instruction string
    impl Robot {
        pub fn run_string(self, s: &str) -> Self {
            s.chars().fold(self, |r, c| match c {
                'L' => r.execute(Instruction::TurnLeft),
                'R' => r.execute(Instruction::TurnRight),
                'A' => r.execute(Instruction::Advance),
                _ => r,
            })
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_advance_north() {
            let r = Robot::new(0, 0, Direction::North).advance();
            assert_eq!(r, Robot::new(0, 1, Direction::North));
        }
    
        #[test]
        fn test_turn_sequence() {
            let r = Robot::new(0, 0, Direction::North);
            let r = r.run(&[
                Instruction::Advance,
                Instruction::TurnRight,
                Instruction::Advance,
                Instruction::Advance,
                Instruction::TurnLeft,
                Instruction::Advance,
            ]);
            assert_eq!((r.x, r.y), (2, 2));
        }
    
        #[test]
        fn test_full_rotation() {
            let r = Robot::new(0, 0, Direction::North);
            let r = r.run(&[Instruction::TurnRight; 4]);
            assert_eq!(r.dir, Direction::North);
        }
    
        #[test]
        fn test_string_instructions() {
            let r = Robot::new(0, 0, Direction::North).run_string("ARAALA");
            assert_eq!((r.x, r.y), (2, 2));
        }
    
        #[test]
        fn test_immutability() {
            let r1 = Robot::new(0, 0, Direction::North);
            let r2 = r1.advance(); // r1 is still valid (Copy)
            assert_eq!(r1.y, 0);
            assert_eq!(r2.y, 1);
        }
    }

    Key Differences

    AspectRustOCaml
    Record updateRobot { x: 5, ..self }{ r with x = 5 }
    ImmutabilityExplicit Copy + value returnDefault for records
    Folditer.fold(init, f)List.fold_left f init lst
    Direction cyclematch on enummatch on variant
    State typeDerives Copy, Clone, PartialEqRecord with field names
    Code length~80 lines~25 lines

    The functional state machine pattern — where each operation returns a new state rather than mutating — maps cleanly to both languages. Rust requires explicit Copy and ..self syntax; OCaml's record update is more terse but both express the same immutable update semantics.

    OCaml Approach

    OCaml records have built-in update syntax: { r with y = r.y + 1 }. This is semantically equivalent to Rust's ..self. List.fold_left execute r instructions threads the robot state through the instruction list. OCaml records are not Copy by default but are value types (immutable by default), so the semantics are the same — each update produces a new value. The OCaml code is more concise because record update syntax is built into the language rather than being a struct initialiser shorthand.

    Full Source

    #![allow(clippy::all)]
    /// Robot Simulator — State with Immutable Records
    ///
    /// Ownership: Robot is a small Copy type (all fields are Copy).
    /// Methods return new robots (functional style) rather than mutating.
    
    #[derive(Debug, Clone, Copy, PartialEq)]
    pub enum Direction {
        North,
        East,
        South,
        West,
    }
    
    #[derive(Debug, Clone, Copy, PartialEq)]
    pub struct Robot {
        pub x: i32,
        pub y: i32,
        pub dir: Direction,
    }
    
    #[derive(Debug, Clone, Copy)]
    pub enum Instruction {
        TurnLeft,
        TurnRight,
        Advance,
    }
    
    impl Direction {
        pub fn turn_right(self) -> Self {
            match self {
                Direction::North => Direction::East,
                Direction::East => Direction::South,
                Direction::South => Direction::West,
                Direction::West => Direction::North,
            }
        }
    
        pub fn turn_left(self) -> Self {
            match self {
                Direction::North => Direction::West,
                Direction::West => Direction::South,
                Direction::South => Direction::East,
                Direction::East => Direction::North,
            }
        }
    }
    
    impl Robot {
        pub fn new(x: i32, y: i32, dir: Direction) -> Self {
            Robot { x, y, dir }
        }
    
        /// Returns a new Robot — functional update (like OCaml { r with ... })
        pub fn advance(self) -> Self {
            match self.dir {
                Direction::North => Robot {
                    y: self.y + 1,
                    ..self
                },
                Direction::East => Robot {
                    x: self.x + 1,
                    ..self
                },
                Direction::South => Robot {
                    y: self.y - 1,
                    ..self
                },
                Direction::West => Robot {
                    x: self.x - 1,
                    ..self
                },
            }
        }
    
        pub fn execute(self, instr: Instruction) -> Self {
            match instr {
                Instruction::TurnLeft => Robot {
                    dir: self.dir.turn_left(),
                    ..self
                },
                Instruction::TurnRight => Robot {
                    dir: self.dir.turn_right(),
                    ..self
                },
                Instruction::Advance => self.advance(),
            }
        }
    
        /// Run a sequence of instructions — fold pattern
        pub fn run(self, instructions: &[Instruction]) -> Self {
            instructions.iter().fold(self, |r, &i| r.execute(i))
        }
    }
    
    /// Version 2: Parse instruction string
    impl Robot {
        pub fn run_string(self, s: &str) -> Self {
            s.chars().fold(self, |r, c| match c {
                'L' => r.execute(Instruction::TurnLeft),
                'R' => r.execute(Instruction::TurnRight),
                'A' => r.execute(Instruction::Advance),
                _ => r,
            })
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_advance_north() {
            let r = Robot::new(0, 0, Direction::North).advance();
            assert_eq!(r, Robot::new(0, 1, Direction::North));
        }
    
        #[test]
        fn test_turn_sequence() {
            let r = Robot::new(0, 0, Direction::North);
            let r = r.run(&[
                Instruction::Advance,
                Instruction::TurnRight,
                Instruction::Advance,
                Instruction::Advance,
                Instruction::TurnLeft,
                Instruction::Advance,
            ]);
            assert_eq!((r.x, r.y), (2, 2));
        }
    
        #[test]
        fn test_full_rotation() {
            let r = Robot::new(0, 0, Direction::North);
            let r = r.run(&[Instruction::TurnRight; 4]);
            assert_eq!(r.dir, Direction::North);
        }
    
        #[test]
        fn test_string_instructions() {
            let r = Robot::new(0, 0, Direction::North).run_string("ARAALA");
            assert_eq!((r.x, r.y), (2, 2));
        }
    
        #[test]
        fn test_immutability() {
            let r1 = Robot::new(0, 0, Direction::North);
            let r2 = r1.advance(); // r1 is still valid (Copy)
            assert_eq!(r1.y, 0);
            assert_eq!(r2.y, 1);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_advance_north() {
            let r = Robot::new(0, 0, Direction::North).advance();
            assert_eq!(r, Robot::new(0, 1, Direction::North));
        }
    
        #[test]
        fn test_turn_sequence() {
            let r = Robot::new(0, 0, Direction::North);
            let r = r.run(&[
                Instruction::Advance,
                Instruction::TurnRight,
                Instruction::Advance,
                Instruction::Advance,
                Instruction::TurnLeft,
                Instruction::Advance,
            ]);
            assert_eq!((r.x, r.y), (2, 2));
        }
    
        #[test]
        fn test_full_rotation() {
            let r = Robot::new(0, 0, Direction::North);
            let r = r.run(&[Instruction::TurnRight; 4]);
            assert_eq!(r.dir, Direction::North);
        }
    
        #[test]
        fn test_string_instructions() {
            let r = Robot::new(0, 0, Direction::North).run_string("ARAALA");
            assert_eq!((r.x, r.y), (2, 2));
        }
    
        #[test]
        fn test_immutability() {
            let r1 = Robot::new(0, 0, Direction::North);
            let r2 = r1.advance(); // r1 is still valid (Copy)
            assert_eq!(r1.y, 0);
            assert_eq!(r2.y, 1);
        }
    }

    Deep Comparison

    Robot Simulator — Comparison

    Core Insight

    Both OCaml and Rust support "functional update" syntax for records/structs, creating a new value with some fields changed. Rust's Copy trait makes small structs behave like OCaml values — no ownership complications.

    OCaml Approach

  • { r with y = r.y + 1 } — record update syntax
  • List.fold_left execute r instructions — fold over instruction list
  • • Pattern matching on variants for direction and instruction
  • • Records are immutable by default
  • Rust Approach

  • Robot { y: self.y + 1, ..self } — struct update syntax
  • #[derive(Copy, Clone)] makes Robot value-like (no moves)
  • instructions.iter().fold(self, |r, &i| r.execute(i))
  • • Methods take self by value (Copy), return new Robot
  • Comparison Table

    AspectOCamlRust
    Update syntax{ r with field = val }Struct { field: val, ..self }
    ImmutabilityDefaultVia Copy + value semantics
    FoldList.fold_left.iter().fold()
    MethodsFree functionsimpl methods
    String parsingNot shownrun_string with char fold

    Learner Notes

  • • Rust struct update ..self copies remaining fields (requires Copy or explicit clone)
  • • Small enums and structs should derive Copy for functional style
  • [Instruction::TurnRight; 4] creates array with 4 copies (requires Copy)
  • • OCaml record update and Rust struct update are remarkably similar
  • Exercises

  • Add bounds checking: return Result<Robot, String> from advance if the robot would leave a grid of size N×N.
  • Implement a reverse_instructions function that, given a final robot state and instruction sequence, returns the starting state.
  • Add a TurnAround instruction that composes two TurnRight calls.
  • Track the path: add run_with_history that returns Vec<Robot> of every state visited.
  • In OCaml, extend the robot to face NorthEast | NorthWest | SouthEast | SouthWest and implement eight-directional movement.
  • Open Source Repos