Recursion

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 rec applyInstruction (robot: Robot) (instructions: char list): Robot =
        match instructions with
        | [] -> robot
        | 'L' :: remainingInstructions -> applyInstruction (turnLeft robot) remainingInstructions
        | 'R' :: remainingInstructions -> applyInstruction (turnRight robot) remainingInstructions
        | 'A' :: remainingInstructions -> applyInstruction (advance robot) remainingInstructions
        | _   -> failwith "Invalid instruction"

    applyInstruction robot (Seq.toList 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 a recursive doMove function that takes a robot and an instruction, and returns an updated robot:

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

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.

Note

To allow a function to recursively call itself, the rec modified must be added. In other words: by default, functions cannot call themselves.

The first parameter of the doMove function is its accumulator parameter: robot. This parameter represents the current robot's state and is updated between the recursive function calls until we're done processing. The second parameter contains the remaining instructions as a char list, which means that we'll be able to pattern match on it.

Within this function, we pattern match on the instructions

match instructions with

We first check to see if there are no remaining instructions, which means that we're done and we can return the robot:

| [] -> robot

The next three patterns map to the three instructions (left/right/advance):

| 'L' :: remainingInstructions -> doMove (turnLeft robot)  remainingInstructions
| 'R' :: remainingInstructions -> doMove (turnRight robot) remainingInstructions
| 'A' :: remainingInstructions -> doMove (advance robot)   remainingInstructions

We check to see if the first element in the list is an 'L', 'R' or 'A' character. If it is, we recursively call doMove, but with a correctly updated robot (e.g. (turnLeft robot)) and the remaining instructions. This will recursively apply the instructions, all whilst updating the robot.

Finally, we'll need to handle the case where we get an instruction that we don't support, in which case we'll just throw an exception:

| _ -> failwith "Invalid instruction"

Putting it all together

The final step is to call our recursive helper function. We'll use [Seq.toList][seq.tolist] to convert the string to char list, and pass in the robot parameter as its initial value:

doMove robot (Seq.toList instructions)

which gives us:

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

    doMove robot (Seq.toList instructions)

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

Step-by-step execution

In case you're wondering how this recursive implementation works, 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). These are the values of the doMove calls:

Instructions Robot Returns
['R'; 'A'; 'R'; 'A'] North, (0, 0) East, (0, 0)
['A'; 'R'; 'A'] East, (0, 0) East, (1, 0)
['R'; 'A'] East, (1, 0) South, (1, 0)
['A'] South, (1, 0) South, (1, 1)
[] South, (1, 1) 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.

11th Sep 2024 · Found it useful?