Fold

Robot Simulator
Robot Simulator in F#
module RobotSimulator

type Direction = North | East | South | West
type Position = int * int

type Robot = Robot of direction: Direction * position: Position

let create (direction: Direction) (position: Position): Robot =
    Robot(direction, position)

let private turnLeft (Robot(direction, position)): Robot =
    match direction with
    | North -> Robot(West,  position)
    | East  -> Robot(North, position)
    | South -> Robot(East,  position)
    | West  -> Robot(South, position)

let private turnRight (Robot(direction, position)): Robot =
    match direction with
    | North -> Robot(East,  position)
    | East  -> Robot(South, position)
    | South -> Robot(West,  position)
    | West  -> Robot(North, position)

let private advance (Robot(direction, (x, y))): Robot =
    match direction with
    | North -> Robot(direction, (x    , y + 1))
    | East  -> Robot(direction, (x + 1, y    ))
    | South -> Robot(direction, (x    , y - 1))
    | West  -> Robot(direction, (x - 1, y    ))

let move (instructions: string) (robot: Robot): Robot =
    let doMove (robot: Robot) (instruction: char): Robot =
        match instruction with
        | 'L' -> turnLeft robot
        | 'R' -> turnRight robot
        | 'A' -> advance robot
        | _   -> failwith "Invalid instruction"

    Seq.fold doMove robot instructions

Defining the Robot type

The first thing we need to do is to decide how we want to represent a robot. Obviously, we'll need to keep track of the robot's direction and position (which types are already defined), but there are several options to choose from:

The class-based approach probably makes the least sense, as you'll likely have mutable state, which isn't the functional-first approach we're aiming for.

The other three are all valid, but let's go with a discriminated union. It's main advantage of tuples is that you can associate names with its fields, and the advantage over records is that you could easily define different types of robots later on, if needed.

Here is what our type looks like:

type Robot = Robot of direction: Direction * position: Position
Note

Whilst not required, we name the fields of the discriminated union. It is a good practice to do, as it can really help with readability.

Creating a robot

Creating a robot is nothing more than passing the direction and position values to the Robot constructor:

let create (direction: Direction) (position: Position): Robot =
    Robot(direction, position)

Turning

Two of the instructions we need to support turn the robot (left or right). In both cases, we'll need to return a new robot with a different direction, but with the same position. We'll use pattern matching on the direction and then call the Robot constructor with the updated values:

let private turnLeft (Robot(direction, position)): Robot =
    match direction with
    | North -> Robot(West,  position)
    | East  -> Robot(North, position)
    | South -> Robot(East,  position)
    | West  -> Robot(South, position)

let private turnRight (Robot(direction, position)): Robot =
    match direction with
    | North -> Robot(East,  position)
    | East  -> Robot(South, position)
    | South -> Robot(West,  position)
    | West  -> Robot(North, position)

Note that we deconstruct our Robot type's fields directly in the function parameter, which is a nice convenience.

Advancing the robot

Advancing the robot changes its x or y position, but retains its direction. Once again, we'll use pattern matching on the direction and then call the Robot constructor with the updated values:

let private advance (Robot(direction, (x, y))): Robot =
    match direction with
    | North -> Robot(direction, (x    , y + 1))
    | East  -> Robot(direction, (x + 1, y    ))
    | South -> Robot(direction, (x    , y - 1))
    | West  -> Robot(direction, (x - 1, y    ))

In case you missed it, we even deconstructed the nested position tuple directly within the parameter definition!

Moving the robot

Finally, let's implement the logic to move the robot according to the instructions. Let's define an doMove function that takes a robot and an instruction, and returns an updated robot:

let doMove (robot: Robot) (instruction: char): Robot =
    match instruction with
    | 'L' -> turnLeft robot
    | 'R' -> turnRight robot
    | 'A' -> advance robot
    | _   -> failwith "Invalid instruction"

Note that we're just deferring to the three functions we just defined.

We'll define this function within the move function (also known as a nested function), but it could just as well have been defined outside the move function.

With this function in place, we can then use Seq.fold to iterate over the instructions string:

Seq.fold doMove robot instructions

which gives us:

let move (instructions: string) (robot: Robot): Robot =
    let doMove (robot: Robot) (instruction: char): Robot =
        match instruction with
        | 'L' -> turnLeft robot
        | 'R' -> turnRight robot
        | 'A' -> advance robot
        | _   -> failwith "Invalid instruction"

    Seq.fold doMove robot instructions

And with that, we can move our robot according to the specified instructions!

Step-by-step execution

In case you're not familiar with Seq.fold, the process of folding is one where you're sequentially processing a sequence, whilst threading along an accumulator value. In our case, the sequence is the instructions string (which is a sequence of chars) and the accumulator value is the robot.

Let's consider the following example:

let robot = create Direction.North (0, 0)
move "RARA" robot

The robot starts facing north and is at position (0, 0).

Instruction New direction New position
'R' East (0, 0)
'A' East (1, 0)
'R' South (1, 0)
'A' South (1, 1)

Alternative: using a tuple

If you'd like to be really minimal, tuples could be used. For convenience and readability, let's define a type abbreviation:

type Robot = Direction * Position

Then all we need to do is remove the Robot() wrappers:

let create (direction: Direction) (position: Position): Robot =
    (direction, position)

let private turnLeft (direction, position): Robot =
    match direction with
    | North -> (West,  position)
    | East  -> (North, position)
    | South -> (East,  position)
    | West  -> (South, position)

let private turnRight (direction, position): Robot =
    match direction with
    | North -> (East,  position)
    | East  -> (South, position)
    | South -> (West,  position)
    | West  -> (North, position)

let private advance (direction, (x, y)): Robot =
    match direction with
    | North -> (direction, (x    , y + 1))
    | East  -> (direction, (x + 1, y    ))
    | South -> (direction, (x    , y - 1))
    | West  -> (direction, (x - 1, y    ))

The rest of the code can be left unchanged.

Alternative: using a record

Instead of using a discriminated union, one could also use a record:

type Robot = { direction: Direction; position: Position }

The create function then uses a record expression to create the record:

let create (direction: Direction) (position: Position): Robot =
    { direction = direction; position = position }

Like a discriminated union, records are immutable, which means that we'll need to return a new record each time we want to make a change. For that, we'll use the copy and update record expressions, which use the with keyword to make a copy of a record but with certain changes:

let private turnLeft (robot: Robot): Robot =
    match robot.direction with
    | North -> { robot with direction = West }
    | East  -> { robot with direction = North }
    | South -> { robot with direction = East }
    | West  -> { robot with direction = South }

We apply the same logic to the turnRight function:

let private turnRight (robot: Robot): Robot =
    match robot.direction with
    | North -> { robot with direction = East }
    | East  -> { robot with direction = South }
    | South -> { robot with direction = West }
    | West  -> { robot with direction = North }

And the last function to update is the advance function:

let private advance (robot: Robot): Robot =
    let x, y = robot.position

    match robot.direction with
    | North -> { robot with position = (x    , y + 1) }
    | East  -> { robot with position = (x + 1, y    ) }
    | South -> { robot with position = (x    , y - 1) }
    | West  -> { robot with position = (x - 1, y    ) }

The rest of the code can be left unchanged.

30th Oct 2024 · Found it useful?