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}