AoC 2021 - Day 2

Day 2 of Advent of Code is here, and it turned out to be pretty easy. Starting off all we get is a list of instructions of the form <instruction> <parameter> where <instruction> is one of forward, up or down and <parameter> is an integer.

My idea to parse this list of instructions was to create a list of tuples of the form (instruction, parameter) using a regular expression ^([a-z]+) ([0-9]+)$. This regular expression makes use of capturing groups to extract the instruction and the parameter separately. The groups are accessed using match.group(1) and match.group(2).

pattern = re.compile('^([a-z]+) ([0-9]+)$')

def load_input() -> list[Tuple[str, int]]:
    with open('../.input/day02', 'r') as f:
        return [
            (match.group(1), int(match.group(2))) for match
            in (pattern.search(line.strip()) for line in f.readlines())
        ]

Task 1

Here my idea was to use a dictionary to store functions (lambdas) under keys 'forward', 'up' and 'down'. The functions are then called with the parameter as argument together with the current position.

I used functools.reduce to fold the list of instructions into a single final position. Reduce represents in Python the mathematical concept of a fold, which also is a commonly used operation in functional programming. The function functools.reduce takes a function, a list and an initial value as arguments. The function is applied to the initial value and the first element of the list, then the result is used as the initial value for the next iteration. The result of the last iteration is returned.

This operation can be represented as foldl f z xs in some other functional languages:

-- Haskell
foldl f z [] = z
foldl f z (x:xs) = foldl f (f z x) xs

Here’s my solution:

def solve1() -> int:
    numbers = load_input()
    instructions = {
        "forward": lambda arg, x, y: (x + arg, y),
        "up": lambda arg, x, y: (x, y + arg),
        "down": lambda arg, x, y: (x, y - arg),
    }

    x, y = reduce(lambda pos, item: instructions[item[0]](item[1], *pos), numbers, (0, 0))
    return abs(x) * abs(y)

The key part is right here:

# unpacking the final position into x and y
x, y = reduce(
  # Function to apply which takes the previous result (accumulator) and the next item in the list.
  # We use dict to get the function from the dictionary.
  # We use *pos to unpack accumulated value into function arguments.
  lambda pos, item: instructions[item[0]](item[1], *pos),
  # list of instructions to fold
  numbers,
  # initial value for the accumulator
  (0, 0)
)

Task 2

Task 2 was pretty much identical to task 1. I used a dictionary to store functions (lambdas) under keys 'forward', 'up' and 'down', except that the functions have different bodies and they take an additional parameter a for aim.

def solve2() -> int:
    numbers = load_input()
    instructions = {
        "forward": lambda arg, x, y, a: (x + arg, y + a * arg, a),
        "up": lambda arg, x, y, a: (x, y, a - arg),
        "down": lambda arg, x, y, a: (x, y, a + arg),
    }

    x, y, _ = reduce(lambda pos, item: instructions[item[0]](item[1], *pos), numbers, (0, 0, 0))
    return abs(x) * abs(y)