Advanced Ruby Hash Techniques

The humble ruby Hash has a few tricks up its sleeve. Far from being a dumb key-value system, the Hash object gives you the power to do some very interesting and sophisticated things.

When you use something as much as Ruby developers use Hashes, it's easy to think you've seen it all.

But I'm here to tell you that the humble ruby Hash has a few tricks up its sleeve. Far from being a dumb key-value system, the Hash object gives you the power to do some very interesting and sophisticated things.

Any object can be a hash key

Before we go any farther I'd like to point out one thing that may not be obvious. While we tend to use strings and symbols as hash keys, that doesn't mean we can't use other kinds of objects as well. In fact, you can use almost anything as a hash key.

# Numbers can be hash keys
{1 => "one"}[1] # "one"

# So can the Ruby kernel
{Kernel => 1}[Kernel] # 1

# You can store values for specific classes
{Kernel => 1, String => 2}["hello world".class] # 2

# You can store values for booleans
{true => "verdad"}[1==1] # "verdad"

# You can even use complex arrays and even other hashes as hash keys
{[[1,0],[0,1]] => "identity matrix"}[[[1,0], [0,1]]] # "identity matrix"

Some of these options are more useful than others, but they're all available to you.

You have control over the default value.

Suppose you have a hash h={ a: 1 }. If you try to access a value that doesn't exist - for example   h[:x]  - you get nil. That's because nil is the default value of every hash, unless you specify otherwise.

You can set the default value for a new hash by passing an argument into its constructor.

h = Hash.new("This attribute intentionally left blank")
h[:a] = 1
h[:a] # 1
h[:x] # "This attribute intentionally left blank"

Dynamic default values

Pay attention, because this is trick is the foundation of everything that follows.

If you pass a block into the constructor, you can generate default values programmatically. In the example below I've added a timestamp to the default value, so you can see that it's being dynamically generated.

h = Hash.new { |hash, key| "#{key}: #{ Time.now.to_i }" }
h[:a] # "a: 1435682937"
h[:a] # "a: 1435682941"
h[:b] # "b: 1435682943"

This is important because the "default value" block can do things other than return a default value.

Raising an exception if a hash key isn't present

One of the main problems with hashes is that they fail silently. You accidentally type in user[:phnoe] instead of user[:phone], and instead of raising an exception, the hash returns nil. But you can change this behavior.

h = Hash.new { |hash, key| raise ArgumentError.new("No hash key: #{ key }") }
h[:a]=1
h[:a] # 1
h[:x] # raises ArgumentError: No hash key: x

This technique can be useful for debugging and refactoring because it applies to a specific hash. It's a much less intrusive way to add this behavior than something like monkey-patching the Hash class would be.

Note: I'm not suggesting that anyone use this in place of Hash.fetch in new code. It's just an interesting trick to have up your sleeve for debugging and refactoring.

Lazily-generated lookup tables

This technique useful for caching the results of a computation. Imagine that you need to calculate a lot of square roots. You could create a lazily-populated lookup table like the example below.

sqrt_lookup = Hash.new { |hash, key| hash[key] = Math.sqrt(key) }
sqrt_lookup[9] # 3.0
sqrt_lookup[7] # 2.6457513110645907
sqrt_lookup    # {9=>3.0, 7=>2.6457513110645907}

Recursive lazy lookup tables

Suppose you have a recursive function and you want to cache the result of each recursion. Let's take a factorial calculation as an example. "Four factorial", aka "4!" is just another way of saying "4x3x2x1." You could implement this recursively using a hash. The example below, which I've taken from this blog post demonstrates it nicely:

factorial = Hash.new do |h,k| 
  if k > 1
    h[k] = h[k-1] * k
  else
    h[k] = 1
  end
end

factorial[4] # 24
factorial    # {1=>1, 2=>2, 3=>6, 4=>24}

Modifying defaults after initialization

You can also control the default value after a hash has been created. To do this use the default and default_proc setters.

h={}
h[:a] # nil
h.default = "new default"
h[:a] # "new default"

h.default_proc = Proc.new { Time.now.to_i }
h[:a] # 1435684014

Find the Ruby: A game of lazily infinite nested hashes

Just for fun, let's wrap all of these useful techniques into one extremely useless example. Remember that old text-based game Adventure? Let's build the stupidest version of it ever.

Imagine you're in a cave. You can go north, south, east or west. Three of these choices take you to a new "room" of the cave where you keep exploring. But one choice leads you to a "ruby." Hence the name of the game: "find the ruby."

Each room in the cave corresponds to a hash. The hash only has one entry. One of ["n", "s", "e", "w"] chosen at random, has the value "You found the ruby."  If you choose incorrectly a new hash is created and added to the tree.

generator = Proc.new do |hash, key| 
  hash[key] = Hash.new(&generator).merge(["n", "s", "e", "w"][rand(4)] => "You found the ruby!")
end
dungeon = Hash.new(&generator)
dungeon["n"] # <Hash ...
dungeon["n"]["s"] # <Hash ...
dungeon["n"]["s"]["w"] # "You found the ruby!"

What to do next:
  1. Try Honeybadger for FREE
    Honeybadger helps you find and fix errors before your users can even report them. Get set up in minutes and check monitoring off your to-do list.
    Start free trial
    Easy 5-minute setup — No credit card required
  2. 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
    Stop wasting time manually checking logs for errors!

    Try the only application health monitoring tool that allows you to track application errors, uptime, and cron jobs in one simple platform.

    • Know when critical errors occur, and which customers are affected.
    • Respond instantly when your systems go down.
    • Improve the health of your systems over time.
    • Fix problems before your customers can report them!

    As developers ourselves, we hated wasting time tracking down errors—so we built the system we always wanted.

    Honeybadger tracks everything you need and nothing you don't, creating one simple solution to keep your application running and error free so you can do what you do best—release new code. Try it free and see for yourself.

    Start free trial
    Simple 5-minute setup — No credit card required

    Learn more

    "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, Cofounder & CTO of YvesBlue

    Honeybadger is trusted by top companies like:

    “Everyone is in love with Honeybadger ... the UI is spot on.”
    Molly Struve, Sr. Site Reliability Engineer, Netflix
    Start free trial