Indifferent With Ruby Hashes

December 13, 2019

4 min read β˜•οΈ

A nice hash, unlike Ruby's πŸ˜‚, Photo by ctwoner on Pixabay

A nice hash, unlike Ruby's πŸ˜‚, Photo by ctwoner on Pixabay

I miss plain old JavaScript objects... πŸ˜”


sigh Ruby hashes

A Ruby hash is a key-value pair container which can be made with either a hash literal {} or constructor Hash.new. They're known as dictionaries or associative-arrays and allow you to store values that can be accessed by a key. If you're acquainted with JSON or JavaScript objects, this should look familiar. When it comes to values, they can be anything, even other nested hashes! Keys are generally either strings or symbols (like a Ruby string, but stronk, docs here) for simplicity, but you can use anything that is an "object type". Reaching for the Ruby docs, we see that:

A user-defined class may be used as a hash key if the hash and eql? methods are overridden to provide meaningful behavior. By default, separate instances refer to separate hash keys.

A typical implementation of hash is based on the object's data while eql? is usually aliased to the overridden == method

This just means many things, even complex objects you or others write, can evaluate to hash keys.

Accessing Hash Values

In the beginning, I mentioned that hashes are referred to as associative-arrays. This is because while arrays, or lists of items, are indexed by numbers, e.g.:

# list has 3 items (a .length equal to 3)
list = ["first", "second", "third"]

# notice below that we are accessing the list items by a number index
# ruby arrays use 0-based indexing, or the first item is the 0th index
first = list[0]
second = list[1]

hashes, on the other hand, use generic keys, e.g.:

# declare a new hash using a hash literal
# I'm only using strings as values here, but
hash = {
  nested: {
    "look ma" => "I'm in a nested hash!"
  },
  foo: "bar", # symbol notation
  "three" => 3, # fat arrow notation, which is necessary for string and number keys
  4 => "four"
}

# get the nested hash using a symbol key
nested_hash = hash[:nested]

# get the three value using a string
three = hash["three"]

# we can even use numbers as keys!
four = hash[4]

Where's the beef? πŸ„

So far, hashes look pretty neat, right? Well, for the most part, they are. They're used all the time, especially in Rails applications. However, what do you think happens when you try the following?

hash = {
  foo: "bar" # a symbol key, :foo
}

# notice: the foo key is a symbol,
# but instead we're trying to access using a string
foo = hash["foo"]

The answer?

Example: Hash access using a string when the key is a symbol

Example: Hash access using a string when the key is a symbol

We can't access the foo value using the string "foo", but instead have to use the symbol :foo. Mildly put, this is annoying.

Coming from JavaScript land, objects have shorthand property names, which looks strikingly similar to symbol notation for Ruby hash keys:

const hash = {
  foo: 'bar',
};

// accessing with object destructuring works
let { foo } = hash;

// as does with a string works
foo = hash['foo'];

// as does named property access
foo = hash.foo;

Example: Node object access

Example: Node object access

Keep in mind that I've purposefully left out ES2015 Symbols, as they are different from Ruby symbols. Mainly, they don't coerce to strings easily. And, when converting them to strings using .toString(), for example, they produce "Symbol(<VALUE>)", e.g.:

Example: Demonstrating JS Symbols

What do you want Cody?

I'll tell you what I want, what I really really want!

I'll tell you what I want, what I really really want!
hash = { foo: "bar" }

this = hash[:foo]
that = hash["foo"]

this == that #=> true

Rails to the Rescue πŸ›€

Rails has a weird symbiotic relationship with Ruby and will constantly monkey-patch the core library, or at least provide extensions to it. The extension that will save us is ActiveSupport::HashWithIndifferentAccess, where:

Implements a hash where keys :foo and "foo" are considered to be the same.

A hash of this type can be made either using:

# the ActiveSupport::HashWithIndifferentAccess constructor
hash = ActiveSupport::HashWithIndifferentAccess.new

# or, a hash literal followed by `.with_indifferent_access`
hash = {}.with_indifferent_access

Here it is in action:

Example: Using the Rails Hash With Indifferent Access Extension

Example: Using the Rails Hash With Indifferent Access Extension

And I LOVE it.

Conclusion

TLDR: Indifferent access hashes are what hashes should have been in the first place.

I shouldn't have to care whether you give a hash :foo or "foo" to access something. Both of those cases should behave identically. If it did, APIs that return hashes wouldn't need to use something like deep_symbolize_keys. Also, why are these highly valuable things not in Ruby core yet??

And that's why I miss POJOs: Plain Old JavaScript Objects. And why I'm so indifferent πŸ˜‰with Ruby hashes.


Comments