For Day 3, we are given a “corrupted” block of memory as a string. We are to find the “uncorrupted instructions” in the block and emulate them.
For this problem, we don’t attempt to force things into the
series paradigm. The cl-ppcre
library provides a bunch
of functions for working with regular expressions, and
the do-register-groups
macro is ideal for this problem.
It iterates over all the matches of a regular expression, binding
submatches to some variables, with some optional processing of the
submatch. (If the cl-ppcre
library offered a function that
returned a series of matches, we could use that, but it offers a
do
macro.)
First, we read the input:
(defun read-input (input-pathname) (read-file-into-string input-pathname))
Next, we define a regular expression to match the instructions:
(defparameter *mul-instruction* "(mul\\((\\d{1,3}),(\\d{1,3})\\))")
Now we just iterate over all the matches:
(defun part-1 () (let ((answer 0)) (cl-ppcre:do-register-groups (whole (#’parse-integer left) (#’parse-integer right)) (*mul-instruction* (read-input (input-pathname))) (declare (ignore whole)) (incf answer (* left right))) answer))
do-register-groups
is an example of a macro where the
parenthesized subexpressions do not indicate function calls. The
first parenthesized subgroup is a variable list, and within the
variable list, a parenthesized subgroup indicates a transformer to
be applied before binding the variable. So in this case, we are
binding the variables whole
, left
,
and right
, and we run the matching subgroup
through parse-integer
before binding
the left
and right
variables.
The second parenthesized subgroup is a list of the regular expression to match and the string to match within. After these irregular parenthesized subgroups, the remaining is a body of code that is executed for each match.
In the body of the code, we ignore the whole
variable
(which is the whole match) and increment the answer by the product
of the left
and right
variables. As is
usual for a do
macro, we transport the data out of the
loop by side effecting a lexically scoped variable.
For part 2, we have additional instructions to emulate. Our loop
will now have some state to it, and we will side effect the state as
we iterate over the matches. We can
just extend the regular expression and add a cond
to the body of the do-register-groups
to handle the
new instructions. As we iterate over the matches, we side
effect the emulation state:
(defparameter *do-mul-instruction* "(do\\(\\))|(don’t\\(\\))|(mul\\((\\d{1,3}),(\\d{1,3})\\))") (defun part-2 () (let ((answer 0) (on t)) (cl-ppcre:do-register-groups (turn-on turn-off whole (#’parse-integer left) (#’parse-integer right)) (*do-mul-instruction* (read-input (input-pathname))) (declare (ignore whole)) (cond (turn-on (setq on t)) (turn-off (setq on nil)) (on (incf answer (* left right))) (t nil))) answer))
Strictly speaking, we don’t need to use side effects. We could rework this to be purely functional, but this seems unnatural. Yes, there is a bit of cognitive overhead in remembering that there is a state variable that determines whether or not we are executing an instruction, but the code is quite understandable.
“Do” macros usually need some side effects, but we can
localize the side effects by using a let
to lexically
bind the state variables and then side effecting them from within
the loop. This is an effective compromise between the functional
and imperative programming paradigms.
No comments:
Post a Comment