Extreme branchless: Mars Rover
Continuing my branchless journey, I wanted to refactor a highly state-based kata: the Mars Rover.
As a reminder, we have a state defined as:
data Rover = Rover
{ position :: Position,
}
data Position = Position
{ x :: Int,
}
deriving stock (Eq)
data Direction = North | East | South | West
Needless to say that Direction
creates a lot of branching:
roverCommands = displayPosition . foldl' go (Rover {x = 0, y = 0, direction = North})
where
go p =
\case
'R' ->
p
{ direction =
case p.direction of
North -> East
East -> South
South -> West
West -> North
}
'L' ->
p
{ direction =
case p.direction of
North -> West
East -> North
South -> East
West -> South
}
'M' ->
let potentialPosition =
case p.direction of
North -> (p.position) {y = p.position.y + 1}
East -> (p.position) {x = p.position.x + 1}
South -> (p.position) {y = p.position.y - 1}
West -> (p.position) {x = p.position.x - 1}
in p {position = fromMaybe p.position $ nextPosition potentialPosition}
c -> error $ "Unknown command " <> show c
We have two patterns matching here: Direction
and Command
.
For Direction
, we can reuse Game of Life Strategy
tactic,
cross-referencing Direction
and defining Position
evolution:
data Direction = Direction
{ displayDirection :: String
, nextRotateLeft :: Direction
, nextRotateRight :: Direction
, moveForward :: Position -> Position
}
We can define each Direction
:
north =
Direction
{ displayDirection = "N"
, nextRotateLeft = west
, nextRotateRight = east
, moveForward = \position -> position {y = position.y + 1}
}
east =
Direction
{ displayDirection = "E"
, nextRotateLeft = north
, nextRotateRight = south
, moveForward = \position -> position {x = position.x + 1}
}
south =
Direction
{ displayDirection = "S"
, nextRotateLeft = east
, nextRotateRight = west
, moveForward = \position -> position {y = position.y - 1}
}
west =
Direction
{ displayDirection = "W"
, nextRotateLeft = south
, nextRotateRight = north
, moveForward = \position -> position {x = position.x - 1}
}
The next step, we have to design the Command
so they'll rely on Direction
instead of pattern matching:
newtype Command = Command { runCommand :: Rover -> Rover }
A Command
is a simple function which evolve a Rover
.
We can define them individually:
cmdLeft =
Command $ \rover -> rover { direction = rover.direction.nextRotateLeft }
cmdRight =
Command $ \rover -> rover { direction = rover.direction.nextRotateRight }
cmdMove nextPosition =
Command $ \rover ->
rover { position = fromMaybe rover.position $ nextPosition $ rover.direction.moveForward rover.position }
It's a straightforward delegation to Direction
, no more logic is needed.
Finally, we can compose everything in our entry-point function:
roverCommands nextPosition =
displayPosition
. foldl' (\rover c -> (parseCommand c).runCommand rover)
(Rover {position = Position {x = 0, y = 0}, direction = north})
where
parseCommand =
\case
'R' -> cmdRight
'L' -> cmdLeft
'M' -> cmdMove nextPosition
c -> error $ "Unknown command " <> show c
Note: we have kept parseCommand
because one of the aim of the kata is to
practice Outside-in TDD,
keeping a stable interface.
In a real-world codebase, I would rework it so split responsibilities:
roverCommands = ...