Though I rarely ever complete it, I like to do some of the challenges put forth for Advent of Code from time to time: https://adventofcode.com/2023 . I didn’t really expect to get stuck on the first day, but I sure did!
Spoilers below. I apologize for nothing. Turn back now.
The problem
At first glance you are given some very simple input (ASCII, alpha-numeric, line-oriented) and expected to memorize the first and last digit for each line. This is very straight-forward but they upgrade the challenge by mixing in words as digits.
My general approach to that complication was to substitute the words for their
corresponding digits first, with just a naive str#replace()
, and then pass
the modified result to my solution from part 1 which should then work Just
Work ™. I knew this was fraught with peril, I even wrote the following
comment:
// NOTE: I am 100% doing this the lazy way because I do not want to
// rewrite this as a full-blown parser to do part 2!
The problem you run into with this approach is that it is position-independent.
The order depends on how your replace-operations are ordered relative to each
other, not how the matches in the string are ordered. I realized this was
probelmatic when I saw that the string from the sample file eightwothree
became eigh23
, instead of 8wo3
, which is invalid for the sample puzzle. I
immediately thought “HAH! They knew I was going to take a shortcut, and made it
so that I would have to write an actual parser instead.” - Clearly this was a
carefully constructed trap to make you realize that earlier matches needed to
consume their tokens in-order.
So I wrote a small parser that looked for tokens, did lookahead, moved the
cursor around, etc. such that eightwothree
would become the obviously
correct 8wo3
. This actually passes the test of the sample file, but fails on
the input file in some edge-cases. This drove me nuts, and I found out that
apparently I was not the only one stuck here. (This is not aided by the fact
that AOC will simply tell you whether your answer, a lone checksum, is high or
low; so you have no idea which line it takes issue with.) The clever solution,
which I eventually adapted, was not a parser at all but just a better use of
simple search & replace.
The key insight is you can insert non-digits in the output stream without
affecting the result, so instead of replacing with naked digit literals you
instead replace them with their leading and traililng characters in-tact. For
instance eight
becomes e8t
, so the t
remains to complete the following
two
. My parser, which I was absolutely convinced was what they wanted, had
actually become a liability!
This was intensely frustrating for two reasons. First of all my brain absolutely does not work like that. I would have never come up w/ a solution to insert extra “unnecessary” characters into the output stream. (In fact I didn’t. Once I understood the desired solution state I just hacked my parser to instead advance only one token at a time and maintain additional state to fix up the next match if were at the end of an existing match.) Secondly I was 110% convinced that I was avoiding an intentional trap by the authors to prevent you from doing the “naive” replacements, so instead of trying to make search-replace work I had completely abandoned it.
I guess this says more about the power of preconceived notions, or perhaps
about jumping to conclusions, than it does about the puzzle design. However,
for the record, TOKENIZING PARSERS CONSUME THEIR INPUT. I will play your silly
game, but I will never acknowledge that eightwothree
is 823
and not
8wo3
. “You idiot, you own an LR parser, the T is fucking gone! It’s just
gone.”
An aside on Rust
I’ll be using Rust for most of these challenges this year, simply because my development environment for it is very full-featured now, however I can’t help but feel like Rust hates this sort of code.
I’m starting to understand why so many seasoned C-developers seem to be so non-excited by Rust. When you have these problem states that are basically a 1D or 2D array of characters Rust makes it really hard to manipulate them. If you feel like doing iterator-golf, chaining monads, writing generic functions, and dealing with extension traits, then sure Rust is great fun! However none of that is the problem I’m trying to solve - it’s friction. (Friction that arguably pushes me to a better, more flexible, more maintainable solution; but when the code is going to be thrown away in fifteen minutes after it has processed the input: it’s just maddening to jump through the hoops.)
I just want to index into the string. “NOoooOO!” Rust protests, “this is UTF-8 do you mean bytes or codepoints or grapheme clusters? You have to download this 100 crate dependency graph and install unicode-segmentation and chain five iterators together and unwrap all these results because string manipulation is actually completely fallible.”
“It’s just some ASCII characters please let me index it”, to which Rust reluctantly
agrees but only if you litter your code with &b"foo"[..]
’s and
"bar".as_bytes()
and maybe a couple .collect::<Vec<_>>()
’s for good
measure. Also you have to pinky promise that you will check the Result when you
go to convert it back to a Real String™.
What’s interesting is I never feel that way when actually programming in Rust, but boy if you’re doing these sorts of “quick string/binary manipulation” tasks is it absolutely infuriating. The entire time I was like “I would have been done with this ages ago if I was using Elixir.” (Which has excellent support for splitting apart “binaries” w/ pattern matching.)
Maybe what I want is a mode of Rust where it’s like “yeah here’s some traits to manipulate strings like an array; it’s probably gonna blow up in your face but have fun.”
All the ceremony of error handling & conversion & allocation is just hilariously missing the mark when you’re talking about a program that is going to run for a few milliseconds and terminate. (Probably with the wrong answer; but who cares.)
So, to all the people saying Rust doesn’t let them iterate quickly enough, I think I get where you’re coming from now. It doesn’t feel this way to me all the time, but for a certain class of problem-solving I’m definitely feeling the “friction.”