Ruby/Rails

Advent of Criminally Bad Ruby Code

By
published

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.

Mild Advent of Code Spoilers

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:

  1. We could define these methods with metaprogramming (using more operator overrides).
  2. 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.