Objects as Ruby Hash Keys

One often-overlooked feature of Ruby's hashes is that you can use any object as a hash key, not just strings and symbols. In this post we examine how Optcarrot, the Ruby NES emulator, uses this feature to optimize its mapped memory implementation.

If you've been following the Ruby 3x3 effort, you've probably heard of Optcarrot. It's an NES emulator written in pure Ruby.

I was recently looking over the Optcarrot source and one interesting detail stuck out to me. It makes extensive use of a feature of Ruby's hashes that is often overlooked, but quite useful. That is the ability to use any object as a hash key.

The context: NES Memory Mapping

As high-level programmers, we tend to think of memory as RAM. But at a lower-level, "memory" has many other uses.

Reading and writing to "memory" is how the NES's CPU communicates with the GPU, the control pads and any special electronics on the cartridge. Depending on the address used, a write_to_memory method call could reset the joystick, swap out VRAM or play a sound.

How would you implement this in Ruby?

Optcarrot does it by storing two Method objects for each of the 65536 addresses. One is a getter and one is a setter. It looks something like this:

@getter_methods[0x0001] = @ram.method(:[])
@setter_methods[0x0001] = @ram.method(:[]=)

The problem: Duplicate Objects

The problem with using Object#method in this way is that it creates a lot of individual Method objects that are identical.

We can see this by looking at object_id:

> a = []
> a.method(:[]=).object_id
=> 70142391223600
> a.method(:[]=).object_id
=> 70142391912420

The two Method objects have different object_id values, so they're different objects even though they do the same thing.

Normally, we might not care about a few extra Method objects, but in this case we're dealing with thousands of them.

The solution: Memoizing via a Hash

Optcarrot avoids the duplicate Method object problem with a trick that's so simple it's easy to overlook.

It uses a hash to memoize and deduplicate. The simplified code below demonstrates the technique:

def initialize
  @setter_methods = []
  @setter_cache = {}

def add_setter(address, setter)
  # Doesn't store duplicates
  @setter_cache[setter] ||= setter

  # Use the deduped version
  @setter_methods[address] = @setter_cache[setter]  

This works because Hash doesn't care what kind of objects you give it as keys.

If this is confusing, try it in IRB with strings:

> cache = {}
> cache["foo"] ||= "bar" 
=> "bar"
cache["foo"] ||= "baz"
=> "bar"

Now, consider that in Ruby, strings are instances of the class String. The mechanism Ruby used to use the string as a hash key is basically the same as the one used to store a Method object.

How Hash calculates equality

When using non-string objects as hash keys, the question arises: how does Hash know if two objects are equal?

The answer is that it uses the Object#hash method. This method goes through your object and recursively generates a hash. It looks like this:

> a.method(:[]=).hash
=> 929915641391564853

Because identical objects produce identical hash values it can be used as a test of equality.

a.hash == b.hash

Interesting enough, this is the same approach used by the eql? method:


This works with the Method objects in our example:

> a.method(:[]=).hash == a.method(:[]=).hash
=> true


Having gotten used to Ruby web development patterns, it was really interesting for me to look at the optcarrot source and see how a real-time non-web app uses different patterns. In a web app I doubt I'll ever make an array with 65536 elements, but here, as part of the setup for a "desktop" app, it makes a lot of sense.

If you have any questions or comments, please be in touch at starr@honeybadger.io or @StarrHorne on Twitter.

Honeybadger has your back when it counts.

We're the only error tracker that combines exception monitoring, uptime monitoring, and cron monitoring into a single, simple to use platform. Our mission: to tame production and make you a better, more productive developer.

Learn more
author photo

Starr Horne

Starr Horne is a Rubyist and Chief JavaScripter at Honeybadger.io. When she's not neck-deep in other people's bugs, she enjoys making furniture with traditional hand-tools, reading history and brewing beer in her garage in Seattle.

More articles by Starr Horne
“We've looked at a lot of error management systems. Honeybadger is head and shoulders above the rest and somehow gets better with every new release.” 
Michael Smith
Try Error Monitoring Free for 15 Days