Skip to content

Instantly share code, notes, and snippets.

@emilong
Last active August 16, 2019 17:07
Show Gist options
  • Save emilong/a893546c29b3317aaccb4b41205c35ab to your computer and use it in GitHub Desktop.
Save emilong/a893546c29b3317aaccb4b41205c35ab to your computer and use it in GitHub Desktop.
Ruby hash surprises

Some possibly surprising things about Ruby hashes

Accessing values

Ruby hashes can have keys of any type, but Strings and Symbols are the most common.

For symbol keys there is a special syntax (similar to Javascript's object literal syntax).

{ "this is a string key" => 3 }

{ :symbol_key => 3 }

# is the same as

{ symbol_key: 3 }

Whether your hash has symbol keys or string keys is not always clear! Many sources of hashes (e.g. HTTP parameter parsing, the database, and others) may provide either.

If you receive a hash that your code did not make and want to get a value, make sure you write a test!

Many errors arise from code like:

# rando_hash == { "very_important_key" => true }
unless rando_hash[:very_important_key]
  # This is executed when we didn't want it to be!
  do_something_scary
end

ActiveSupport can help us out here with HashWithIndifferentAccess which lets you treat Symbols and Strings interchangably if they have the same string value. The only trick is knowing when you have one of these, but if you're not sure and definitely want one, you can make any hash in Rails into a HashWithIndifferentAccess

# rando_hash == { "very_important_key" => true }
indifferent_rando_hash = rando_hash.with_indifferent_access

unless indifferent_rando_hash[:very_important_key]
  # Phew! Now we don't execute this!
  do_something_scary
end

Default values

Many times when you're using a hash to count things or group things, it's easier just to have a value like 0 or a new empty array initialized as the value for each key. Ruby has just the thing for this, but it might not be clear what's happening when you first see it.

hash_for_counting_stuff = Hash.new(0)
objects_with_dates.each do |object|
  hash_for_counting_stuff[object.happened_at.month] += 1
end

# or with each_with_object
hash_for_counting_stuff = 
  objects_with_dates.each_with_object(Hash.new(0)) do |object, hash|
    hash[object.happened_at.month] += 1
  end
# the block is executed for every new key referenced
#
# h is the hash itself
# k is the new key
#
# this sets the value of the new key to a _new_ array.
#
hash_for_grouping_stuff = Hash.new { |h, k| h[k] = [] }

#
# This is valid ruby but never what you want!
#
# hash_for_grouping_stuff = Hash.new([])
# 
# See this post for more details:
#
# https://medium.com/klaxit-techblog/a-headache-in-ruby-hash-default-values-bf2706660392
#

objects_with_dates.each do |object|
  hash_for_grouping_stuff[object.happened_at.month] << object
end

Those aren't keyword arguments... (maybe)

Passing arguments to a method has a special syntax in Ruby where you can pass some of the final parameters as key value pairs that are collected into a hash and passed to the method.

def my_awesome_method(key, hash)
  hash[key]
end

# this is a method call with our 2 parameters
my_awesome_method(:hiya, whats: :happening, hiya: :yall)
# returns :yall

# this is the same thing, but more explicit
my_awesome_method(:hiya, { whats: :happening, hiya: :yall })
# returns :yall

Be careful when you have keyword arguments! They're not the same as hashes, but the syntax can be the same from the calling side!

def my_keyword_method(
  i_need_this_to_be_provided:,
  this_one_is_optional: nil,
  this_one_has_a_default: "hi"
)
  [
    i_need_this_to_be_provided,
    this_one_is_optional,
    this_one_has_a_default
  ].compact.join(" ")
end

# raises because a keyword is missing
my_keyword_method 

# fine and defaults are provided for unspecified params
my_keyword_method(i_need_this_to_be_provided: "ok here it is")

# this raises because my_random_key isn't a keyword specified in the method definition
# and the arguments are not a hash!
my_keyword_method(
  i_need_this_to_be_provided: "ok here it is",
  my_random_key: "let's dance!"
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment