083 — Robot Simulator
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
..self struct update syntax for functional record updates in RustCopy for small state structs to enable pass-by-value semanticsIterator::fold..self spread to OCaml's { r with field = value } updateCode 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
| Aspect | Rust | OCaml |
|---|---|---|
| Record update | Robot { x: 5, ..self } | { r with x = 5 } |
| Immutability | Explicit Copy + value return | Default for records |
| Fold | iter.fold(init, f) | List.fold_left f init lst |
| Direction cycle | match on enum | match on variant |
| State type | Derives Copy, Clone, PartialEq | Record 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);
}
}#[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 syntaxList.fold_left execute r instructions — fold over instruction listRust 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))self by value (Copy), return new RobotComparison Table
| Aspect | OCaml | Rust |
|---|---|---|
| Update syntax | { r with field = val } | Struct { field: val, ..self } |
| Immutability | Default | Via Copy + value semantics |
| Fold | List.fold_left | .iter().fold() |
| Methods | Free functions | impl methods |
| String parsing | Not shown | run_string with char fold |
Learner Notes
..self copies remaining fields (requires Copy or explicit clone)[Instruction::TurnRight; 4] creates array with 4 copies (requires Copy)Exercises
Result<Robot, String> from advance if the robot would leave a grid of size N×N.reverse_instructions function that, given a final robot state and instruction sequence, returns the starting state.TurnAround instruction that composes two TurnRight calls.run_with_history that returns Vec<Robot> of every state visited.NorthEast | NorthWest | SouthEast | SouthWest and implement eight-directional movement.