Understanding Ruby Refinements by implementing hash#with_indifferent_access

As Rails developers, most of us must have come across this handy method, Hash#with_indifferent_access, another instance of "rails-magic" that lets us access values from a ruby hash using either strings or symbols.

require 'active_support/core_ext/hash'

:0> 🚘 = {
:1*  make: 'emoji',
:1*  model: 'Red-car'
:1> }.with_indifferent_access
=> {"make"=>"emoji", "model"=>"red-car"}

:0> puts 🚘[:make]
=> "emoji"
:0> puts 🚘["make"]
=> "emoji"

In this post, I will be discussing three different ways to implement this functionality and compare and contrast each of these techniques.

Patching the core Hash class / Monkey Patch

The most straightforward approach of the three is implementing this functionality by patching the core Hash class.

class Hash 
  def [](key)
    dig(key.to_s) || dig(key.to_sym)
  end

  def []=(key, value)
    dig(key.to_sym) ? store(key.to_sym, value) : store(key.to_s, value)
  end
end

In the above code snippet, we are opening and re-implementing a core class. A side effect of this approach is, from that point when this class is loaded, all instances of Hash implements this new behavior and thereby violates the Open/Closed Principle (OCP).

A technique to extend the behavior of Hash without violating OCP is by using inheritance. This is also how Rails provides indifferent access.

The Rails Way

If we open Rails console and check the class name of the object returned by calling #with_indifferent_access, we see that, it is no longer Hash, but it is ActiveSupport::HashWithIndifferentAccess.

[1] pry(main)> {}.class.name
=> "Hash"
[2] pry(main)> {}.with_indifferent_access.class.name
=> "ActiveSupport::HashWithIndifferentAccess"

If we dig up the source code we can see that, HashWithIndifferentAccess inherits from Hash.

module ActiveSupport
  class HashWithIndifferentAccess < Hash
    def initialize
      # ...
    end
    
    def [](key)
      dig(key.to_s) || dig(key.to_sym)
    end

    def []=(key, value)
      dig(key.to_sym) ? store(key.to_sym, value) : store(key.to_s, value)
    end

    # NOTE:
    # The actual HashWithIndifferentAccess implementation 
    # does a lot more than re-implement #[] and #[]=
  end
end

class Hash
  def with_indifferent_access
    HashWithIndifferentAccess.new(self)
  end
end

This technique doesn’t change the implementation of any of the interfaces of the core class, but has a major downside, which is one needs to install and use ActiveSupport gem.

While working on a gem, I thought it would be a bit too much to include ActiveSupport as a dependency just for the ability to access a hash value with either a string or a symbol and that is when I came across Ruby Refinements.

Let's Refine Hash!

Ruby Refinements, as the name suggests allow us to modify the functionality of a class with very minimal impact. In fact, refinements were introduced to minimize the impact of monkey-patching core classes.

module DifferentlyAccessibleHash
  refine Hash do
    def [](key)
      dig(key.to_s) || dig(key.to_sym)
    end

    def []=(key, value)
      dig(key.to_sym) ? store(key.to_sym, value) : store(key.to_s, value)
    end
  end
end

Take a note of the Module#refine keyword in line#2. Adapting Ruby documentation for our example above, we have the following.

  • Module#refine keyword is used to refine an existing class Hash.
  • Module#refine creates an anonymous module and this anonymous module can be "activated" by employing the using keyword.

This is a bit confusing, correct? Let's try to explain this with a code snippet.

# differently_accessible_hash.rb

module DifferentlyAccessibleHash
  refine Hash do
    def [](key)
      dig(key.to_s) || dig(key.to_sym)
    end

    def []=(key, value)
      dig(key.to_sym) ? store(key.to_sym, value) : store(key.to_s, value)
    end
  end
end

# car.rb

require './differently_accessible_hash'

class EmojiCar

  using DifferentlyAccessibleHash
  
  def initialize
    @specs = { 
     make: 'emoji',
     model: 'Red-car'
    }
  end
  
  def make
    @specs[:make]   # access make via symbol
  end
  
  def model
    @specs['model'] # access model via string 
  end
end

Even though Module#refine creates an anonymous module with our new functionality against Hash, this module is not activated until the usage of using keyword.

Another advantage of the using keyword is that, it is lexically scoped. From the documentation.

  • You may activate refinements at top-level, and inside classes and modules.
  • You may not activate refinements in method scope.
  • Refinements are activated until the end of the current class or module definition, or until the end of the current file if used at the top-level.
  • Refinements are only active within a scope after the call to using. Any code before the using statement will not have the refinement activated

In our example code, since we have "activated" DifferentlyAccessibleHash in the class level, any Hash objects can be accessed with either string or symbol within that class.

Additional Reading

Refine and Using Methods in Ruby by Mehdi Farsi
So what’s the deal with Ruby refinements, anyway? by Avdi Grimm

Show Comments