Advent of Criminally Bad Ruby Code
Yesterday, Marco Roth and I committed a variety of Ruby programming felonies and are now fugitives from the Rubocops. Marco joined my Twitch stream where I was working on the fifth day of this year’s Advent of Code. He had some very good ideas about how to tackle the problem. I was keen to try them out.
This blog post is focused on the features of Ruby that we used, not the puzzle itself. There will be some allusions to the Advent of Code puzzle, but the code is so obfuscated that this likely won’t seriously spoil the solution for you. Viewer discretion is advised.
Smooth Operators
My goal for the session was to override some operators. Ruby let’s you redefine what operators like +
and -
do on your objects. This is a great feature. It means that if you have objects that represent complex values, you can implement common operations on them.
Money objects are a great example. Imagine you have objects that represent monetary values, which are a numeric value and a currency. You want to be able to add those together if their currencies match. Overriding the +
method let’s you write five_us_dollars + ten_us_dollars
. It’s handy.
Part of the input for the puzzle is a series of “rules” that are two integers with a pipe (|
) between them, like 35|47
. It would take me no time to come up with a regular expression to parse out the two numbers and then convert them to integers, but that’s not what I wanted to do.
I wanted to execute the expression using eval
and have it return a Rule
object containing both numbers. You might be wondering, “what the hell, Jared?” I don’t have time to justify this. Instead, go listen to Noah Gibbs talk about programming as art. I’m making art.
First, we tried redefining the bitwise OR operator on the Integer class and… boom. My debugger exploded. It turns out that something inside of the REPL depends on bitwise OR on Integer. So that won’t work.
What we needed was the ability to monkey-patch a class only in the context of our code. Fortunately, Ruby has a solution for that. (I’m actually not sure it’s fortunate. Maybe “unfortunately” is a better word here.) Enter Refinements.
Sleek and Refined
Refinements are essentially monkey-patches that you can enable in the current scope (top-level or class/module-level, but not method-level) and they are only available in that scope. Once control passes out of that scope, it’s as if the refinement doesn’t exist until control returns to that scope.
module Hacks
refine Integer do
def |(other)
Rule.new(self, other)
end
end
end
using Hacks
You activate a refinement with the using
keyword. All the code in this file/module/class after using Hacks
will use our definition of |
on Integer, but we won’t break all the code that uses that operator in gems and other third-party code.
Operator Overload
Our Rule class was pretty simple at this point. It was just a container for two integers. In order to solve the problem, we’d need access to those two integers. We could expose a couple of reader methods, but where’s the fun in that?
class Rule
def initialize(first, last)
@first = first
@last = last
end
def +@
@first
end
def -@
@last
end
end
Instead, we overrode the unary +
and -
operators to return each integer, with +
as the integer that is supposed to come first, and -
as the integer that’s supposed to come last. This allowed us to write +some_rule
and -some_rule
to extract the integers out of each rule.
Like I said before, this ‘aint business; this is art.
If you’re looking to override some operators, Kevin Newton has a nice list of Ruby’s operators on his blog that specifically lists out the “call name operators”. These are the operators that are “syntactic sugar” for method calls and can be overridden on any object.
A Wide Array of Bad Code
Ruby’s standard library is full of useful classes and methods for solving Advent of Code problems. You almost always use something from Array or Enumerable. Our day five solution ended up using sum
, map
, all?
, partition
, select
, find
, none?
, and a few others, but I didn’t want to litter my solution with these totally comprehensible words. What’s a boy to do?
The obvious solution is to start overriding all the operators that Array doesn’t already implement and using them to do other things. For example, we wanted to use the greater than operator, rather than map
.
module Hacks
refine Array do
def >(other)
map { other.call(_1) }
end
end
end
This allows to put proc on the right side of a greater than expression and have it function like Array#map
. I’m basically Picasso.
[1, 2, 3, 4] > ->(n) { n + 1 }
#=> [2, 3, 4, 5]
It turned out, lots of Array methods could be massacred like this. We made sum
into >=
, partition
into =~
, select
into /
, and didn’t stop there. We made some_array ** value
return the index of that value in the array. We made some_array < value
delete that value from the array.
In our most creative moment, we made it so that some_array % "middle"
would return the middle element in that array, but some_array % ->(n) { n >= 5 }
would mutate the array, removing all elements that are less than five. If you like this code, Super Good is open for new contracts. Give me a shout.
Metaprogramming for Evil
At this point, we’d overridden about ten different methods on Array using refinements and seven of those all took the exact same form:
def some_operator(other)
some_array_method { other.call _1 }
end
We realized two things about this:
- We could define these methods with metaprogramming (using more operator overrides).
- We could support blocks and procs with these methods.
This led to this beauty, which I’ll explain after I give you a second to look upon my Works, ye Mighty, and despair!
def self.[](operator, array_method)
define_method(operator) do |other = nil, &block|
send(array_method) {
block ? block.call(_1) : other.call(_1)
}
end
end
This allowed us to use the operator method on the Array class itself to define new operators that alias existing Array methods that support both blocks and procs. This turned most of our new operators methods into this gibberish:
self[:>=, :sum]
self[:>, :map]
self[:<=, :all?]
self[:=~, :partition]
self[:/, :select]
self[:!~, :find]
self[:>>, :none?]
It also meant that the following two lines (that you should never actually write in production code) became functionally equivalent, though they are not equivalent from a performance perspective. (Who cares?)
[1, 2, 3, 4] >= ->(n) { n * 2 }
[1, 2, 3, 4].>= { _1 * 2 }
I can hear the sirens in the distance. This isn’t just art. This is transgressive art.
Symbol Vomit
Our crimes against Ruby coalesced into this chunk of code. It’s technically most of our solution to both part one and two, but it probably won’t even slightly help you solve the puzzle.
rule_data, update_data = *-input
rules = rule_data > ->(r) { eval r }
updates = update_data > ->(u) { eval "Update[#{u}]" }
valid_updates, invalid_updates = updates =~ ->(update) {
rules <= ->(rule) { update === rule }
}
part_one = valid_updates.>=(&:~@)
part_two = (invalid_updates > ->(update) {
update << rules
}).>=(&:~@)
`Part One: #{part_one}`
`Part Two: #{part_two}`
Did I mention that we overrode the backtick operator (`
) too? We also used argument forwarding (...
), Symbol#to_proc
with our operators (&:~@
), splatting, and probably a few other esoteric Ruby features. The end result was some code that looks vaguely like Haskell if you squint.
This whole exercise begs one question: should you write Ruby like this? I’m here to tell you that that’s the wrong question. The right question is, “what other crimes can we commit with refinements and overriding operators?” There’s a whole world of tremendously obtuse and difficult-to-understand code just waiting to be written. Go write it!
Future Crimes
I’d like to thank Marco again for his help with this solution. I was really just riffing off his suggestions. Go subscribe to my Twitch channel if you want to see what else we can come up with or contribute to future solutions. My Mastodon and Bluesky are the best places to find out when I’ll be streaming.
Marco suggested that we abuse use method_missing
, and I think we might do that for day six. I also want to implement some totally unnecessary virtual machines, do some code generation, and find some way to eval
an entire input file. Basically, I want to attack and dethrone God.
Keep Ruby Weird, baby.