loading...
Factorial

A trick with Ruby Hash.new

nflamel profile image Fran C. ・2 min read

Awesome Ruby tricks (3 Part Series)

1) A trick with Ruby array literals 2) A trick with Ruby Hash.new 3) A trick with Ruby anonymous classes

Hashes are used a lot in Ruby (sometimes even abused) and they have a very interesting functionality that is rarely used: Hash.new has 3 different forms

Regular form

It just returns an empty hash whose unexisting keys return always nil.

h = Hash.new # Or h = {}
h[:hello] # => nil

This is just equivalent to using an empty Hash literal also known as {}.

Fixed default

It allows for a parameter which is returned in case the key doesn't exist.

h = Hash.new('world')
h[:hello] # => 'world'

This form needs to be used carefully though since you always return the same object, which means if you modify it, you modify it for all subsequent calls:

h[:hello].upcase! # "WORLD"
h[:foo] # "WORLD"

That is why only recommend using this option in a case: maps of classes.

POLICIES = Hash.new(ForbidAllPolicy).merge({
  admin: AccessAllPolicy,
  user: RestrictedPolicy
})

policy = POLICIES[current_user.role]

POLICIES[:user]
# => RestrictedPolicy

POLICIES[:hacker]
# => ForbidAllPolicy

Why classes but not other objects? Because classes are singletons (a singleton is an object that only has one instance at the same time) so you do not care that you're always going to get the very same object all the time.

Calculated default

This form gives the biggest freedom since it allows us to pass a block to calculate the value and even to store it on the hash. So the next call to that same key will already have a value.

h = Hash.new do |hash, key|
  value = key.upcase
  puts "'#{key}' => '#{value}'"

  hash[key] = value
end
h["hello"]
# prints -> 'hello' => 'HELLO'
# => "HELLO

# Next call to the same key is already assigned, the block isn't executed

h["hello"]
# => "HELLO"

My preferred use case for this is to protect myself from nils and to avoid continuous nil checks.

In a case like this:

h = {}
h[:hello] << :world
# => NoMethodError (undefined method `<<' for nil:NilClass)

You can either ensure the key is initialized

h = {}
h[:hello] ||= []
h[:hello] << :world
h
# => {:hello=>[:world]}

Or use the trick we just learned to ensure you will never have a nil and you get a sane default instead.

h = Hash.new { |h, k| h[k] = [] }
h[:hello] << :world
h
# => {:hello=>[:world]}

Take into account that passing around a Hash like this might be dangerous as well. Nobody will expect that a Hash returns something for a key that doesn't exist, it can be confusing and hard to debug.

If we get keys for the previous hash:

h.keys
# => [:hello]

How to use it then? Just do not let the rest of the world know it is a Hash 😬

class CachedUser
  def initialize
    @cache = Hash.new { |h, id| h[id] = User.find(k) }
  end

  def fetch(id)
    @cache[id]
  end
end

cache = CachedUser.new

cache.fetch(1)
# => select * from users where id=1
# => <User: @id=1>

cache.fetch(1)
# => <User: @id=1>

Although the example is extremely simple it showcases how you can safely use a Hash as a container object safely without exposing some of its drawbacks but profiting from its flexibility.

Awesome Ruby tricks (3 Part Series)

1) A trick with Ruby array literals 2) A trick with Ruby Hash.new 3) A trick with Ruby anonymous classes

Posted on Oct 15 '19 by:

nflamel profile

Fran C.

@nflamel

I break code for a living... wait no, I fix it, most of the times. Well, sometimes.Yeah, I break code for a living.

Factorial

Everything you need to manage your HR processes Spend less time doing administrative HR tasks and focus on what matters.

Discussion

markdown guide