Advent of Code Day 1–Easter Bunny HQ
I’ve set up a GitHub repository for my F# solutions to Advent of Code this year, and although this year I suspect I won’t have enough time to blog about every day’s solution, I did attempt the first day’s problem.
As usual, the first part is simply parsing the problem input. For this problem, there was a string of directions like this: “L2, R3” which means turn left, go forward two blocks, turn right, go forward three blocks.
I needed to turn this into a sequence of “instructions”. Initially I used string splitting, and my instruction type encapsulated the change of direction and the number of blocks to move. But as it turned out, part 2 of the problem is more easily solved if there is a separate “turn” instruction, and instead of a “move forward n blocks” command, it should repeat the “move forward 1 block” command n times.
So by using a bit of regular expressions and a Seq.collect
to expand the move commands, we can get them into a sequence of Instruction
types like this:
open System.Text.RegularExpressions
type Turn = Right | Left
type Instruction = Turn of Turn | Move
let input = System.IO.File.ReadAllText (__SOURCE_DIRECTORY__ + "\\input.txt")
let instructions = Regex.Matches(input, "([LR])|(\\d+)")
|> Seq.cast<Match>
|> Seq.map (fun m -> m.Value)
|> Seq.collect (function | "L" -> [Turn Left] | "R" -> [Turn Right] | n -> [for _ in 1..int(n) -> Move])
Now the state we need to maintain as we apply these commands is what our current position is, what direction we are facing, and (for part b of the problem), all the places we have visited so far.
This can be done by using a tuple for the current position, a vector indicating the direction we are facing (which eliminates some clumsy match statements from my original solution which created a discriminated union of North | West | South | East
), and an immutable set of all locations we have visited so far. Here’s my State
type and the starting state:
type State = { pos: int*int; facing: int*int; visited: Set<int*int> }
let startState = { pos = (0,0); facing = (0,-1); visited = Set.empty }
Now we need a move
function that takes an instruction and a current state, and applies it, creating a new state. The instructions are either turn, or move one step forwards. So I created a couple of helper functions – one to rotate the direction, and one to add two vectors, and used them to implement the move
function:
let rotate (x,y) = function | Right -> (y*(-1),x) | Left -> (y,x*(-1))
let move state instruction =
match instruction with
| Turn direction ->
{ state with facing = rotate state.facing direction }
| Move ->
let addv (x,y) (i,j) = (x+i,y+j)
{ state with pos = addv state.pos state.facing; visited = Set.add state.pos state.visited }
Once we can apply one instruction, it is trivial to apply all the instructions with a fold
, and get the finishing state from which we only need the current position to solve part a:
let endState = instructions |> Seq.fold move startState
let getDistance (x,y) = abs(x) + abs(y)
printfn "Part a: Distance from home is %d" (getDistance endState.pos)
Part b required us to remember all the places we had been (which is why we maintain the visited
set in the state object), and identify the first place we visit twice. We can achieve this by using a scan
, which produces a sequence of states as we apply each instruction. That means we can use find
to iterate through until we find the first one where the current position is in the list of already visited places.
let visitedTwice = instructions
|> Seq.scan move startState
|> Seq.find (fun f -> f.visited.Contains(f.pos))
printfn "Part b: Distance from home is %d" (getDistance visitedTwice.pos)
You can see the full code here, and as always I welcome ideas about how my F# can be improved.
One of the interesting things about solving this challenge in F# was the realisation that I would have been able to solve it much quicker with procedural instead of functional code. I’d simply create a for loop to go through each instruction, mutating a current position and facing direction variable and adding positions to an already visited list.
By contrast, the functional solution was harder to refactor from part a to part b, since the fold
function I’d used wasn’t appropriate any more, and I had to significantly change my code to suit it to solving both halves of the problem.
So although I believe a functional paradigm has a lot of benefits over procedural, this challenge highlights that my brain is still wired to think procedurally, and it will be a while yet before I find the functional approach comes more naturally.