Advent of Code 2021 Day 4: One-line Python hell
Advent of Code is a yearly programming challenge involving 25 puzzles, released daily in December. I published all my solutions for 2021 on GitHub. I’m featuring some of the more interesting or odd ones in a series of posts.
For some reason I committed to writing solutions to the first few problems in one-line quasi-functional Python (which, incidentally, is Turing complete). Day 4’s one-liner is the most hideous of the bunch.
The problem asks us to simulate a gaggle of bingo-playing grannies: given a sequence of draws
and a set of bingo boards
, we submit the score
of whoever wins first.
Here’s the code, annotated with pointers to explanations below.
( # (1)
lambda draws, boards: reduce( # (2)
lambda score_and_boards, draw: (
( # (3)
lambda boards: (
reduce( # (4)
lambda score, board: sum(
n for row in board for (n, seen) in row if not seen
)
* draw
if score is None
and any(
all(seen for (_, seen) in row)
for b in [board, zip(*board)] # (5)
for row in b
)
else score,
boards,
None,
),
boards,
)
)( # (6)
lmap( # (7)
lambda board: lmap(
lambda row: lmap(
lambda e: (e[0], draw == e[0] or e[1]), row
),
board,
),
score_and_boards[1],
)
)
if score_and_boards[0] is None
else score_and_boards # (8)
),
draws,
(None, boards),
)[0]
)(*read("input.txt"))
-
The form
(lambda t: f(t))(g(x))
allows us to writef(g(x))
more concisely whenf(t)
has a lot oft
s. Here, this lets me avoid writingread("input.txt")[1]
multiple times when I meanboards
. This pattern is like a less elegant form oflet ... in
orwhere
from Haskell.read
is just boilerplate. It returns(draws, boards)
, wheredraws
is the list of numbers drawn andboards
is a list of bingo board states, represented by 5x5 listlists of tuples(n, seen)
withseen
initiallyFalse
. -
This is
functools.reduce
, which is just a fold in FP lingo;reduce(f, [x] + xs, a) == reduce(f, xs, f(a, x))
.In our case
f
is the lambda at (3), which maps tuplesscore_and_boards := (winning score: Maybe int, current board states: [[[(int, bool)]]])
and the currentdraw
to a newscore_and_boards
tuple. The iterable to be reduced isdraws
, and the initial value ofscore_and_boards
is(None, boards)
, which indicates that there is initially no winning score and all boards are unmarked.At the end of the reduction, we pluck off the
score
from the final tuple. - We use the composition trick from (1) again here.
- This
reduce
searches the updated board states for a winning board, returning thescore
of the firstboard
with a complete row or column ofseen
entries, orNone
if none exists. We assume that there is no tie for first. zip(*board)
is the transpose ofboard
; its rows are columns ofboard
.- To update board states, for each board we flip the
seen
bit of each entry if matches the currentdraw
. - Our
lmap
is just the compositionlist . map
. These lines would be less obnoxious as a list comprehension. - We stop searching for a win if someone has already won.
Here’s the equivalent procedural code:
draws, boards = read("input.txt") # (1)
for draw in draws: # (2)
for i, board in enumerate(boards): # (4)
boards[i] = [ # (6)
[(n, draw == n or seen) for (n, seen) in row]
for row in board
]
if any(
all(seen for (_, seen) in row)
for b in [boards[i], zip(*boards[i])] # (5)
for row in b
):
return draw * sum(
n for row in boards[i] for (n, seen) in row if not seen
)
A third as many lines as the “one-liner” (according to black, at least), and perhaps a bit more readable.