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 = {}
  ...
end

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

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

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:

a.eql?(b)

This works with the Method objects in our example:

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

Conclusion

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.

Get the Honeybadger newsletter

Each month we share news, best practices, and stories from the DevOps & monitoring community—exclusively for developers like you.
    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
    An advertisement for Honeybadger that reads 'Turn your logs into events.'

    "Splunk-like querying without having to sell my kidneys? nice"

    That’s a direct quote from someone who just saw Honeybadger Insights. It’s a bit like Papertrail or DataDog—but with just the good parts and a reasonable price tag.

    Best of all, Insights logging is available on our free tier as part of a comprehensive monitoring suite including error tracking, uptime monitoring, status pages, and more.

    Start logging for FREE
    Simple 5-minute setup — No credit card required