class Hash

Practical examples and pitfalls for Hash

Hash examples: distinguish missing keys and build independent groups

Practical notes by Ruby-Doc.org

Give missing options a meaning

Assume a reporting job could accept an optional limit. Omitting a key allows the default to be used, but explicitly setting nil means no limit. Using || to fall back from looking up the key loses these distinctions. Additionally, || will replace the stored false, which is a valid state for many switches.

Example 1
settings = {limit: nil, verbose: false}
p settings.fetch(:limit, 100)
p settings.fetch(:verbose, true)
p settings.fetch(:retries, 3)
Expected output
nil
false
3

Use fetch when the presence or absence of a key matters. If you have a switch that must always exist, do not provide a fallback, allowing KeyError to indicate the missing field. When a fallback is expensive, a block provides the ability to avoid computing it when the key exists. The application must determine whether a found value has the correct type; successful retrieval is not validation.

Create separate buckets for each group

A shipment report requires one parcel list per depot. Use a default block to create and store the list the first time a depot is encountered. Subsequent lookups for a depot retrieve its stored list. A different depot gets its own array.

Example 2
groups = Hash.new { |hash, depot| hash[depot] = [] }
[["north", "P17"], ["south", "P18"], ["north", "P19"]].each do |depot, parcel|
  groups[depot] << parcel
end
p groups["north"]
p groups["south"]
Expected output
["P17", "P19"]
["P18"]

Do not substitute a single shared mutable default such as Hash.new([]) for this design. Appending to that default would append to the same array for unrelated missing keys without adding an entry for each. In the grouping example, reading a missing key creates an entry, so use a different lookup if reading must leave absent groups absent.

Decide how conflicting updates combine

A merge of configurations is not necessarily a recursive merge. merge produces a new outer hash and assigns later values over earlier ones unless a block provides additional rules. Hashes or arrays nested within other hashes or arrays require their own policies. For example, replacing a retry policy entirely may be safer than silently retaining existing nested settings.

Test an omitted key, a key that holds nil, a key that holds false, and an overlapping nested value. Also ensure that at the input boundary string and symbol keys remain consistent: "limit" and :limit are two distinct keys, not interchangeable spellings.

API reference: Hash API reference

Related: Array · JSON

A Hash object maps each of its unique keys to a specific value.

A hash has certain similarities to an Array, but:

Hash Data Syntax

The original syntax for a hash entry uses the “hash rocket,” =>:

h = {:foo => 0, :bar => 1, :baz => 2}
h # => {foo: 0, bar: 1, baz: 2}

Alternatively, but only for a key that’s a symbol, you can use a newer JSON-style syntax, where each bareword becomes a symbol:

h = {foo: 0, bar: 1, baz: 2}
h # => {foo: 0, bar: 1, baz: 2}

You can also use a string in place of a bareword:

h = {'foo': 0, 'bar': 1, 'baz': 2}
h # => {foo: 0, bar: 1, baz: 2}

And you can mix the styles:

h = {foo: 0, :bar => 1, 'baz': 2}
h # => {foo: 0, bar: 1, baz: 2}

But it’s an error to try the JSON-style syntax for a key that’s not a bareword or a string:

# Raises SyntaxError (syntax error, unexpected ':', expecting =>):
h = {0: 'zero'}

The value can be omitted, meaning that value will be fetched from the context by the name of the key:

x = 0
y = 100
h = {x:, y:}
h # => {x: 0, y: 100}

Common Uses

You can use a hash to give names to objects:

person = {name: 'Matz', language: 'Ruby'}
person # => {name: "Matz", language: "Ruby"}

You can use a hash to give names to method arguments:

def some_method(hash)
  p hash
end
some_method({foo: 0, bar: 1, baz: 2}) # => {foo: 0, bar: 1, baz: 2}

Note: when the last argument in a method call is a hash, the curly braces may be omitted:

some_method(foo: 0, bar: 1, baz: 2) # => {foo: 0, bar: 1, baz: 2}

You can use a hash to initialize an object:

class Dev
  attr_accessor :name, :language
  def initialize(hash)
    self.name = hash[:name]
    self.language = hash[:language]
  end
end
matz = Dev.new(name: 'Matz', language: 'Ruby')
matz # => #<Dev: @name="Matz", @language="Ruby">

Creating a Hash

You can create a Hash object explicitly with:

You can convert certain objects to hashes with:

You can create a hash by calling method Hash.new:

# Create an empty hash.
h = Hash.new
h # => {}
h.class # => Hash

You can create a hash by calling method Hash.[]:

# Create an empty hash.
h = Hash[]
h # => {}
# Create a hash with initial entries.
h = Hash[foo: 0, bar: 1, baz: 2]
h # => {foo: 0, bar: 1, baz: 2}

You can create a hash by using its literal form (curly braces):

# Create an empty hash.
h = {}
h # => {}
# Create a +Hash+ with initial entries.
h = {foo: 0, bar: 1, baz: 2}
h # => {foo: 0, bar: 1, baz: 2}

Hash Value Basics

The simplest way to retrieve a hash value (instance method []):

h = {foo: 0, bar: 1, baz: 2}
h[:foo] # => 0

The simplest way to create or update a hash value (instance method []=):

h = {foo: 0, bar: 1, baz: 2}
h[:bat] = 3 # => 3
h # => {foo: 0, bar: 1, baz: 2, bat: 3}
h[:foo] = 4 # => 4
h # => {foo: 4, bar: 1, baz: 2, bat: 3}

The simplest way to delete a hash entry (instance method delete):

h = {foo: 0, bar: 1, baz: 2}
h.delete(:bar) # => 1
h # => {foo: 0, baz: 2}

Entry Order

A Hash object presents its entries in the order of their creation. This is seen in:

A new hash has its initial ordering per the given entries:

h = Hash[foo: 0, bar: 1]
h # => {foo: 0, bar: 1}

New entries are added at the end:

h[:baz] = 2
h # => {foo: 0, bar: 1, baz: 2}

Updating a value does not affect the order:

h[:baz] = 3
h # => {foo: 0, bar: 1, baz: 3}

But re-creating a deleted entry can affect the order:

h.delete(:foo)
h[:foo] = 5
h # => {bar: 1, baz: 3, foo: 5}

Hash Keys

Hash Key Equivalence

Two objects are treated as the same hash key when their hash value is identical and the two objects are eql? to each other.

Modifying an Active Hash Key

Modifying a Hash key while it is in use damages the hash’s index.

This Hash has keys that are Arrays:

a0 = [ :foo, :bar ]
a1 = [ :baz, :bat ]
h = {a0 => 0, a1 => 1}
h.include?(a0) # => true
h[a0] # => 0
a0.hash # => 110002110

Modifying array element a0[0] changes its hash value:

a0[0] = :bam
a0.hash # => 1069447059

And damages the Hash index:

h.include?(a0) # => false
h[a0] # => nil

You can repair the hash index using method rehash:

h.rehash # => {[:bam, :bar]=>0, [:baz, :bat]=>1}
h.include?(a0) # => true
h[a0] # => 0

A String key is always safe. That’s because an unfrozen String passed as a key will be replaced by a duplicated and frozen String:

s = 'foo'
s.frozen? # => false
h = {s => 0}
first_key = h.keys.first
first_key.frozen? # => true

User-Defined Hash Keys

To be usable as a Hash key, objects must implement the methods hash and eql?. Note: this requirement does not apply if the Hash uses compare_by_identity since comparison will then rely on the keys’ object id instead of hash and eql?.

Object defines basic implementation for hash and eq? that makes each object a distinct key. Typically, user-defined classes will want to override these methods to provide meaningful behavior, or for example inherit Struct that has useful definitions for these.

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

class Book
  attr_reader :author, :title

  def initialize(author, title)
    @author = author
    @title = title
  end

  def ==(other)
    self.class === other &&
      other.author == @author &&
      other.title == @title
  end

  alias eql? ==

  def hash
    [self.class, @author, @title].hash
  end
end

book1 = Book.new 'matz', 'Ruby in a Nutshell'
book2 = Book.new 'matz', 'Ruby in a Nutshell'

reviews = {}

reviews[book1] = 'Great reference!'
reviews[book2] = 'Nice and compact!'

reviews.length #=> 1

Key Not Found?

When a method tries to retrieve and return the value for a key and that key is found, the returned value is the value associated with the key.

But what if the key is not found? In that case, certain methods will return a default value while other will raise a KeyError.

Nil Return Value

If you want nil returned for a not-found key, you can call:

You can override these behaviors for [], dig, and values_at (but not assoc); see Hash Default.

KeyError

If you want KeyError raised for a not-found key, you can call:

Hash Default

For certain methods ([], dig, and values_at), the return value for a not-found key is determined by two hash properties:

In the simple case, both values are nil, and the methods return nil for a not-found key; see Nil Return Value above.

Note that this entire section (“Hash Default”):

Any-Key Default

You can define an any-key default for a hash; that is, a value that will be returned for any not-found key:

You can set the default value when the hash is created with Hash.new and option default_value, or later with method default=.

Note: although the value of default may be any object, it may not be a good idea to use a mutable object.

Per-Key Defaults

You can define a per-key default for a hash; that is, a Proc that will return a value based on the key itself.

You can set the default proc when the hash is created with Hash.new and a block, or later with method default_proc=.

Note that the proc can modify self, but modifying self in this way is not thread-safe; multiple threads can concurrently call into the default proc for the same key.

Method Default

For two methods, you can specify a default value for a not-found key that has effect only for a single method call (and not for any subsequent calls):

What’s Here

First, what’s elsewhere. Class Hash:

Here, class Hash provides methods that are useful for:

Class Hash also includes methods from module Enumerable.

Methods for Creating a Hash

Methods for Setting Hash State

Methods for Querying

Methods for Comparing

Methods for Fetching

Methods for Assigning

Methods for Deleting

These methods remove entries from self:

These methods return a copy of self with some entries removed:

Methods for Iterating

Methods for Converting

Methods for Transforming Keys and Values