Neil's musings

Snippets from a computing educator and researcher.

Advent of Code 2019 day 2

December 02, 2019 in #advent of code #haskell

I wasn't expecting to be writing mondaic State code on day 2! On the other hand, having done several Advent of Codes earlier, I had some previous code I could reuse. This problem is very similar to 2017 day 5 (code), but I'm keeping an eye on 2018 day 21 (code) for how this machine gets extended in future challenges.

Parsing

Megaparsec continues to be overkill for the input, but still yields a very readable parser:

memoryP = integer `sepBy` comma

(The memory is a bunch of integers separated by commas.)

Data structures

I'm not making a clever choice here, and representing the memory as a key-value store, where each key is a memory index and the value is the value at this index. This is called variously a dict, a Hash, an Object, and, in Haskell's case, a Map. (This is different from the map higher-order function.) As the indices are integers, I'm using the Data.IntMap.Strict library.

The machine as a whole is a record, comprising the memory and the instruction pointer:

type Memory = M.IntMap Int

data Machine = Machine { _memory :: Memory
                       , _ip :: Int
                       } 
               deriving (Show, Eq)

Given that we're doing lots of indirection for lookups and updates, I define a couple of utility functions as a bit of syntacic sugar.

-- prefix version of (!)
lkup k m = m!k

-- indirect lookup
(!>) m k = m!(m!k)

-- indirect insert
iInsert k v m = M.insert (m!k) v m

State monad

I use the standard State monad to run the machine. It saves plumbing the state of the machine through the computation. I could use something like a fold or even iterate to move from one machine state to another, but the monadic approach could well be neater in the long run.

A ProgrammedMachine is the monad, taking a machine, performing some operation on it, and returning both the modified machine and the return value. As there's no sensible return value, I use () for that.

runAll repeatedly runs a machine until the instruction has opcode 99. (I don't worry about trying to access locations outside the defined memory.) To run the machine, run one step, then continue running the whole thing.

runStep handles updating the machine, passing the detail of the operation to the perform function. There's one clause there for each operation we know about. Note the use of !> and iInsert to keep this code cleaner. perform is also responsible for updating the instruction pointer, following the hint in part 2 about future operations having different lengths.

type ProgrammedMachine = State Machine ()

runAll :: ProgrammedMachine
runAll = do m0 <- get
            unless (lkup (_ip m0) (_memory m0) == 99)
                do runStep
                   runAll

runStep :: ProgrammedMachine
runStep = 
    do m0 <- get
       let mem = _memory m0
       let ip = _ip m0
       let (mem', ip') = perform (mem!ip) ip mem
       put m0 {_ip = ip', _memory = mem'}

perform :: Int -> Int -> Memory -> (Memory, Int)
perform 1 ip mem = (iInsert (ip + 3) (a + b) mem, ip + 4)
    where a = mem!>(ip + 1)
          b = mem!>(ip + 2)
perform 2 ip mem = (iInsert (ip + 3) (a * b) mem, ip + 4)
    where a = mem!>(ip + 1)
          b = mem!>(ip + 2)

I run the machine by calling execState runAll machine: this runs the machine and returns its final state, discarding any value set by the monad.

Part 1

That's enough to run the Part 1 challenge, with the added wrinkle of updating the memory before execution, and returning the value in memory location 0.

part1 machine = (_memory $ execState runAll machine1202)!0
    where machine1202 = machine { _memory = M.insert 1 12 $ M.insert 2 2 $ _memory machine }

Part 2

The part 2 challenge prompted some refactoring, extracting functions to set the noun and verb of the machine (machineNounVerb), to find the final location 0 result (machineOutput), and to glue those two parts together (nounVerbResult).

nounVerbResult :: Int -> Int -> Machine -> Int
nounVerbResult noun verb machine = machineOutput nvMachine
    where nvMachine0 = machineNounVerb machine noun verb
          nvMachine = execState runAll nvMachine0

machineNounVerb :: Machine -> Int -> Int -> Machine
machineNounVerb machine noun verb = machine { _memory = M.insert 1 noun $ M.insert 2 verb $ _memory machine }

machineOutput :: Machine -> Int
machineOutput machine = (_memory machine)!0

The actual solution to Part 2 is just an exhaustive search of all the noun-verb combinations, returning the first one that satisfies the requirement. (I trust the puzzle designer to have a unique solution.)

part1 = nounVerbResult 12 2

part2Target = 19690720

part2 machine = noun * 100 + verb
    where (noun, verb) = head $ [(n, v) | n <- [0..99], v <- [0..99],
                                          nounVerbResult n v machine == part2Target ]

That also allowed me to simplify the part1 code.

Full code

The full code is on Github.

Share on Google+