GDCR 2023: My participation summary
As every year, the Global day of code retreat is a good excuse to work and rework the same kata a full day.
This year I have attended to the one organized by the Software Crafters Lyon (I'm a member of the organizing team, but I didn't organize the Code Retreat this time).
Subject
It was Codurance's Mars Rover Kata.
I have previously attempted it multiple times in the past, but I did not enjoy it.
My iterations
- Language: Python
- Paired with a freshman student
- It was an exploratory session
- During the introduction, he enumerated a long list of programming he knew, but since he did not have a configured development environment I have chosen Python as I knew it comes with a testing library
- We focused on the rover: making it move forward, teleport at boundaries
- I feel it was more an introduction to industry development
- Language: Java
- Paired with a junior developer
- Additional constraints: Immutable and/or Primitive Obsession
- We focused on the rover: making it move forward, teleport at boundaries, rotate
- We chose Immutable and it was a real challenge for my pair
- Language: Rust
- Paired with a mid-level embedded software developer (C)
- Additional constraints: Evil TDD and/or no control-flow
- We focused on the rover: making it move forward, teleport at boundaries, rotate
- We picked both constraints, while no control-flow was natural, Evil TDD was really fun for me:
- not because I have tried to trap my pair, on the contrary, I have tried to follow a classic TDD style, but at some point it was too uncomfortable for him and he went for a giant step
- the fun part was in his effort to come with complex tests, while I came up with quick and simple implementations
- Language: Scala
- Paired with a mid-level software developer and former part of my group I have already paired with a big number of times
- Additional constraints: Test Commit or Revert and/or Ping-Pong
- We focused on the rover: making it move forward, teleport at boundaries, rotate
- We chose both constraints, but instead of the recommended 2 minutes for the TCR, I have used 4 minutes because I have not done any Scala for a long time, and I'm not used to Mac keyboard layout
- Language: TypeScript
- Paired with a senior frontend software developer and part of my group I have already paired with a couple of times
- Additional constraints: Random constraints (I have got "one level of indentation" and my pair "no control-flow"), but we have judged it was too easy, so we have added "no return"
- We focused on the rover: making it move forward, teleport at boundaries, rotate
- I think it was the most creative session, we have ended up with a map of lambdas for each command
- Language: F#
- I was a mob programming with 10-12 other developers
- Additional constraints: Evil TDD / Everything named with one letter
- It was a fun session, we have started with the rover (again) and we went up to deal with obstacles as a simple list
- It was interesting to see that even if our tests name were not helpful, it was key to understand the structure of our code
- Also, we have seen that functions extractions did not help much for structuring code comprehension
My feedback
Unlike last year, I did not have big expectations, consequently, I did not get frustrated to achieve less and less, and I have had a lot of fun along the day.
I have given a ROTI of 4/5.
My attempt
Here is my attempt, let's start with the Rover, without command:
spec =
describe "Mars Rover Kata" $ do
it "No command should be at start" $
roverCommands "" `shouldBe` "0:0:N"
roverCommands _ = "0:0:N"
It's hard-coded, let's write another one with a move:
-- ...
it "Moving once should be in 0:1:N" $
roverCommands "M" `shouldBe` "0:1:N"
roverCommands =
\case
"M" -> "0:1:N"
_ -> "0:0:N"
Still enumerating, but we can refactor our tests:
spec =
describe "Mars Rover Kata" $ do
forM_
[ ("No command", "", "0:0:N"),
("Moving once", "M", "0:1:N")
]
$ \(name, commands, result) ->
it (name <> " should be " <> result) $
roverCommands commands `shouldBe` result
roverCommands =
\case
"M" -> "0:1:N"
_ -> "0:0:N"
Let's move without wrapping:
("Moving four times (not wrapping)", "MMMM", "0:4:N")
roverCommands =
\case
xs -> "0:" <> show (length xs) <> ":N"
Let's move with wrapping:
("Moving five times (wrapping)", "MMMMM", "0:0:N")
roverCommands =
\case
xs -> "0:" <> show (length xs `mod` 5) <> ":N"
We can rotate once:
("Rotating Right", "R", "0:0:E")
roverCommands =
\case
"R" -> "0:0:E"
xs -> "0:" <> show (length xs `mod` 5) <> ":N"
We can try rotating and moving forward:
("Rotating Right and move", "RM", "1:0:E")
roverCommands = displayPosition . foldl' go (Rover {x = 0, y = 0, direction = North})
where
go p =
\case
'R' -> p {direction = East}
_ ->
case p.direction of
North -> p {y = (p.y + 1) `mod` 5}
East -> p {x = p.x + 1}
data Direction = North | East
data Rover = Rover
{ x :: Int,
}
displayPosition p = show p.x <> ":" <> show p.y <> ":" <> displayedPosition
where
displayedPosition =
case p.direction of
North -> "N"
East -> "E"
That's a huge step, anyway, let's skip wrapping and other rotations:
spec =
describe "Mars Rover Kata" $ do
forM_
[ ("No command", "", "0:0:N"),
("Moving once", "M", "0:1:N"),
("Moving four times (not wrapping)", "MMMM", "0:4:N"),
("Moving five times (wrapping)", "MMMMM", "0:0:N"),
("Rotating Right", "R", "0:0:E"),
("Rotating Right and move", "RM", "1:0:E"),
("Rotating Right and move four times (not wrapping)", "RMMMM", "4:0:E"),
("Rotating Right and move five times (wrapping)", "RMMMMM", "0:0:E"),
("Rotating Left", "L", "0:0:W"),
("Rotating Left and move (wrapping)", "LM", "4:0:W"),
("Rotating Left and move five times (not wrapping twice)", "LMMMMM", "0:0:W"),
("Rotating Left and move six times (wrapping twice)", "LMMMMMM", "4:0:W"),
("Rotating Left twice", "LL", "0:0:S"),
("Rotating Left twice and move (wrapping)", "LLM", "0:4:S"),
("Rotating Left twice and move five times (not wrapping twice)", "LLMMMMM", "0:0:S"),
("Rotating Left twice and move six times (wrapping twice)", "LLMMMMMM", "0:4:S"),
("Rotating Right twice", "RR", "0:0:S"),
("Rotating Right three times", "RRR", "0:0:W")
]
$ \(name, commands, result) ->
it (name <> " should be " <> result) $
roverCommands commands `shouldBe` result
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
}
_ ->
case p.direction of
North -> p {y = (p.y + 1) `mod` 5}
East -> p {x = (p.x + 1) `mod` 5}
South -> p {y = (p.y - 1) `mod` 5}
West -> p {x = (p.x - 1) `mod` 5}
data Direction = North | East | South | West
data Rover = Rover
{ x :: Int,
}
displayPosition p = show p.x <> ":" <> show p.y <> ":" <> displayedPosition
where
displayedPosition =
case p.direction of
North -> "N"
East -> "E"
South -> "S"
West -> "W"
Finally, we can deal with obstacle:
-- ...
("Blocked rover have to rotate", [Position 0 1], "MRM", "1:0:E")
]
$ \(name, obstacles, commands, result) ->
it (name <> " should be " <> result) $
roverCommands obstacles commands `shouldBe` result
roverCommands obstacles =
displayPosition . foldl' go (Rover {position = Position {x = 0, y = 0}, direction = North})
where
go p =
\case
-- ...
'M' ->
let nextRover =
case p.direction of
North -> p {position = (p.position) {y = (p.position.y + 1) `mod` 5}}
East -> p {position = (p.position) {x = (p.position.x + 1) `mod` 5}}
South -> p {position = (p.position) {y = (p.position.y - 1) `mod` 5}}
West -> p {position = (p.position) {x = (p.position.x - 1) `mod` 5}}
in if notElem nextRover.position obstacles
then nextRover
else p
c -> error $ "Unknown command " <> show c
data Rover = Rover
{ position :: Position,
}
data Position = Position
{ x :: Int,
}
deriving stock (Eq)
I had to extract Position
from Rover
, that's why I never start with data
types when I design, because they are bound to my functions (and the system's flow).
I could create Grid
and delegate it new position computation:
-- ...
$ \(name, obstacles, commands, result) ->
it (name <> " should be " <> result) $
roverCommands (computeNextPosition $ Grid obstacles) commands `shouldBe` result
roverCommands nextPosition =
-- ...
'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
newtype Grid = Grid {obstacles :: [Position]}
computeNextPosition grid p =
if notElem normalizedPosition grid.obstacles
then Just normalizedPosition
else Nothing
where
normalizedPosition = Position {x = p.x `mod` 5, y = p.y `mod` 5}