CodeTips#1: Exploring Hashes in Ruby

Hello, fellow readers! Before we can jump right in, we have an announcement!

There is a new series of posts here in the blog: CodeTips!
We will be approaching technical aspects and details about our favorite languages and technologies and their use cases (or no-use cases) but in a more focused/shorter manner. This post is the first of the series and I hope you all enjoy it!

So, without further ado, let’s get started! (๑>◡<๑)

Hashes

In this post, we will be exploring a couple of different ways to fetch values from hashes, their uses, and applications. But before anything, let’s play a little bit with them.

Hashes are very neat data structures that map a unique key to a given value.

There are some similarities between Hashes and Arrays (you can easily implement an array using hashes, btw), but the main difference is that for hashes in Ruby, the index/key for a value can be pretty much any object. And I mean, ANY:

_proc = -> () { 'This is a proc' }
# => #<Proc:0x000055a947450218 (irb):1 (lambda)>
another_hash = { foo: :bar }
# => {:foo=>:bar}
an_integer = 123456
# => 123456
a_string = 'Hello World'
# => "Hello World"
a_symbol = :hi
# => :hi

my_mad_hash = {
  _proc => 'The Proc',
  another_hash => 'The Hash',
  an_integer => 'The Int',
  a_string => 'The String',
  a_symbol => 'The Symbol'
} # Yep, never do this kind of stuff irl. Please (⊙_◎)
# => {#<Proc:0x000055a947450218 (irb):1 (lambda)>=>"The Proc", {:foo=>:bar}=>"The Hash", 123456=>"The Int", "Hello World"=>"The String", :hi=>"The Symbol"}

my_mad_hash[_proc]
# => "The Proc"
my_mad_hash[another_hash]
# => "The Hash"
my_mad_hash[an_integer]
# => "The Int"
my_mad_hash[a_string]
# => "The String"
my_mad_hash[a_symbol]
# => "The Symbol"

You must have noticed in the code above how easy it is to create and retrieve values from a hash. Changing them and adding new ones is just as simple:

my_much_more_reasonable_hash = {
  the_proc: _proc,
  the_hash: another_hash,
  the_int: an_integer,
  the_string: a_string,
  the_symbol: a_symbol
} # Let's make a more manageable hash ಠ_ಠ
# => {:the_proc=>#<Proc:0x000055a947450218 (irb):1 (lambda)>, :the_hash=>{:foo=>:bar}, :the_int=>123456, :the_string=>"Hello World", :the_symbol=>:hi}

my_much_more_reasonable_hash[:the_proc].call
# => "This is a proc"

another_proc = -> (*n) { "The result of the sum is #{n.reduce(&:+)}" }
# => #<Proc:0x000055a94748c2e0 (irb):2 (lambda)>

my_much_more_reasonable_hash[:the_proc] = another_proc
# => #<Proc:0x000055a94748c2e0 (irb):2 (lambda)>

my_much_more_reasonable_hash[:the_proc].(1, 2, 3, 4, 5)
# => "The result of the sum is 15"

# If you try to retrieve a value for a key that does not exist within the hash, it will return a default value (nil)
my_much_more_reasonable_hash[:no_such]
# => nil

# Let me know in the comments if you guys and girls would be interested in more tips about procs, lambdas, and blocks in Ruby (☞⌐■_■)☞

ProTip: There are two styles of creating in-line hashes in Ruby: the "HashRocket" style (like my_mad_hash) and the JSON style (like my_much_more_reasonable_hash). The use of each style depends on what kinds of keys you are using in your hash and your overall preference. Check the Useful Links section at the end of the post for another cool article about it.

So, yeah. Hashes are very cool. Now, let’s get to the meat and potatoes and try a couple of different ways of retrieving values from hashes.

Where are my values?

We’ll be exploring only the methods that focus on retrieving single values from hashes. We can talk about methods like #select, #filter, #reject another day.
The methods we’ll focus on are #fetch, #dig, and #[]

[]

We already saw the simplest way to retrieve a value by a given key in a hash. By simpling using the operator [] we can retrieve any values stored, and if no key is found, the hash will return its default value, usually nil.

Bonus tip: You can change the default value of a hash using the #default= method. This will hardly be recommended in real-life production environments because it can confuse why some hash’s default values are different from others and lead to bugs and regression. There’s probably a situation where this could be useful, but overall do not mess with it ┐( ̄ヮ ̄)┌

fetch

Talking about defaults, this is the method you are looking for to reduce your CyclomaticComplexity and AbcSize if rubocop have been complaining about it ((╮°-°)╮┳━┳ put the table back, I’m joking).

It allows you to define a default value when the key you are looking for is not found.

person = {
  age: 33,
  first_name: 'John',
  last_name: 'Doe',
  height_cm: 190
}
# => {:age=>33, :first_name=>"John", :last_name=>"Doe", :height_cm=>190}

person.fetch(:blood_type, 'Missing or Undefined')
# => "Missing or Undefined"

# Notice that if the key exists but it's value is nil of false, it'll return actual value, not the defined default:
person[:blood_type] = nil
person
# => {:age=>33, :first_name=>"John", :last_name=>"Doe", :height_cm=>190, :blood_type=>nil}

person.fetch(:blood_type, 'Missing or Undefined')
# => nil

This method is useful when you are not sure if a key exists in the dataset you are working with and you need to provide a default value in the case it does not. If you still need to provide a truthy value if the key exists and its value is falsy, maybe it would be better to use a combination of #[] and an or short-circuit:

person[:blood_type] || 'Missing or Undefined'
# => "Missing or Undefined"

dig

This is, without a doubt, my favorite method of the three. As the name suggests, this method allows you to dig into deep nested hashes and safelly retrieve values from them. Check it out:

deep_hash = {
  level1: {
    level2: {
      foo: [:quite, :deep, :values, { another: :hash }],
      bar: [1, 2, 3, 4, 5]
    }
  }
}
# => {:level1=>{:level2=>{:foo=>[:quite, :deep, :values, {:another=>:hash}], :bar=>[1, 2, 3, 4, 5]}}}

deep_hash.dig(:level1, :level2, :foo)
# => [:quite, :deep, :values, {:another=>:hash}]

# It works with neted arrays as well. This is the magic of #default_proc. We also can talk about it another day.
deep_hash.dig(:level1, :level2, :foo, 3, :another)
# => :hash

Yeah yeah. I can hear you: "Why not just use the brackets? (◉-◉)".

ʕ•ᴥ•ʔ (Bear) with me:

deep_hash
# => {:level1=>{:level2=>{:foo=>[:quite, :deep, :values, {:another=>:hash}], :bar=>[1, 2, 3, 4, 5]}}}

# Let's search for an inexistent level

deep_hash.dig(:level1, :level2, :level3, :foo)
# => nil
# Okay, so dig behaves just fine. It will use default values¹ for keys that are not present.

deep_hash[:level1][:level2][:level3][:foo]
# (irb):85:in <main>': undefined method []' for nil:NilClass (NoMethodError)
#   from /home/luan/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/irb-1.3.5/exe/irb:11:in `<top (required)>'
#   from /home/luan/.rbenv/versions/3.0.1/bin/irb:23:in `load'
#   from /home/luan/.rbenv/versions/3.0.1/bin/irb:23:in `<main>'

# Oops. Well, that makes sense. deep_hash[:level1][:level2][:level3] should eval to nil and since nil does not respond to #[] we have an exception.

And that’s why. The best thing about #dig is that, if used correctly, it can help you to avoid nil related bugs in your application and write more confident code.

Bonus: #slice

I know, I know. I said I was not going to explore methods that retrieve multiple values from a hash, but technically it only returns one value (even if it’s another hash |_・))

So, #slice is a very useful way of retrieving only the keys/values you want from a hash. The key does not exist? Do not panic, it will not throw any exceptions:

stop_r = {
  country: ["Russia"],
  animal: ["Raven"],
  adjective: ["Radiant"],
  famous_person: ["Ryan Reynolds"], # does that counts as two points?
  food: ["Ratatouille"],
  movie: ["Ratatouille"],
  color: ["Rust", "Red"] # is Rust a color?
}
# => {:country=>["Russia"], :animal=>["Raven"], :adjective=>["Radiant"], :famous_person=>["Ryan Reynolds"], :food=>["Ratatouille"], :movie=>["Ratatouille"], :color=>["Rust", "Red"]}

stop_r.slice(:country, :adjective)
# => {:country=>["Russia"], :adjective=>["Radiant"]}

stop_r.slice(:food, :movie, :city) # there's no :city key in the hash
# => {:food=>["Ratatouille"], :movie=>["Ratatouille"]}

Slice is particularly useful when rendering JSONs and assigning multiple arguments to methods from hashes that might have lots of other information within

def string_address(country:, state:, city:, street:, details: nil)
  "#{street}, #{city}, #{state}, #{country}#{ details ? ", #{details}" : nil}"
end
# => :string_address

info = {
  country: "Brazil",
  state: "Rio de Janeiro",
  city: "Rio de Janeiro",
  street: "Copacabana",
  age: 33,
  first_name: 'John',
  last_name: 'Doe',
  height_cm: 190
}
# => {:country=>"Brazil", :state=>"Rio de Janeiro", :city=>"Rio de Janeiro", :street=>"Copacabana", :age=>33, :first_name=>"John", :last_name=>"Doe", :height_cm=>190}

# We can use the double splat operator to assign all the attributes at once, but if we do it with info as is:
string_address(**info)
# (irb):103:in `string_address': unknown keywords: :age, :first_name, :last_name, :height_cm (ArgumentError)
#   from (irb):119:in `<main>'
#   from /home/luan/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/irb-1.3.5/exe/irb:11:in `<top (required)>'
#   from /home/luan/.rbenv/versions/3.0.1/bin/irb:23:in `load'
#   from /home/luan/.rbenv/versions/3.0.1/bin/irb:23:in `<main>'

# So, we can slice only the ones we need:
string_address(**info.slice(:country, :state, :city, :street))
# => "Copacabana, Rio de Janeiro, Rio de Janeiro, Brazil"

Of course, this method above could have been written in a way that would allow for such a thing (again, using the double splat operator **), but sometimes this is not under our control.

Conclusion

Well, that’s it. Have fun exploring your hashes, and let me know your thoughts on the subject in the comments below.

As said before, I hope you all will enjoy this new series of posts and that you have enjoyed reading this one.

Until next time! Be safe y’all! ( ̄▽ ̄)/

Useful links:

References:

¹ https://ruby-doc.org/core-3.1.0/Hash.html#class-Hash-label-Default+Values

We want to work with you. Check out our "What We Do" section!