Skip to main content
View
Chapter 4 / 16Hashes
1. Variables and data types
2. Strings and symbols
3. Arrays
4. Hashes
5. Conditionals
6. Loops and iterators
7. Methods and blocks
8. Classes and objects
9. Inheritance
10. Modules
11. Extending code with prepend
12. Enumerable
13. Where to go next
14. Error handling
15. Reading and writing files
16. Advanced class features

Hashes

This chapter introduces Hashes, a structure that associates keys with values. Where an Array stores elements by position (index 0, 1, 2...), a Hash stores them by name.

Principle

With an Array, you access an element by its number: team[0]. This is handy for ordered lists, but not for describing an object. To group a Pokemon's information (name, level, type), you would need to remember that index 0 is the name, index 1 the level, etc. This is fragile and unreadable.

A Hash solves this problem: each value is associated with a key that you choose. You access the value by its key, not by its position. It is like a dictionary: you look up a word (the key) to find its definition (the value).

Creating a Hash

The most common way to create a Hash uses curly braces {} with Symbol keys:

pikachu = { name: 'Pikachu', type: :electric, level: 25 }
p pikachu # => {:name=>"Pikachu", :type=>:electric, :level=>25}
  • name: is the key (a Symbol :name), 'Pikachu' is the value.
  • Key-value pairs are separated by commas.
  • The syntax name: 'Pikachu' is a shortcut for :name => 'Pikachu'. Both forms are equivalent.

You can also write keys with the rocket syntax =>:

# Rocket syntax (older, but still used for non-Symbol keys)
pikachu = { :name => 'Pikachu', :type => :electric }

# With String keys (rare, but possible)
translations = { 'fire' => 'feu', 'water' => 'eau' }
  • The shorthand syntax name: value only works with Symbol keys. For String or Integer keys, you must use =>.
  • In practice, Symbol keys with the shorthand syntax are used almost all the time.

An empty Hash is created with {}:

empty = {}
p empty # => {}

Accessing values

You access a value by passing its key in square brackets:

pikachu = { name: 'Pikachu', type: :electric, level: 25 }

puts pikachu[:name] # => Pikachu
puts pikachu[:level] # => 25
puts pikachu[:ability] # => nil (the key does not exist)
  • If the key does not exist, [] returns nil without error. This is silent, which can be a trap if you make a typo.

To avoid this trap, you can use .fetch, which raises an error if the key does not exist:

pikachu = { name: 'Pikachu', type: :electric, level: 25 }

puts pikachu.fetch(:name) # => Pikachu
# pikachu.fetch(:ability) # => Error! KeyError

# With a default value, no error
puts pikachu.fetch(:ability, 'aucun') # => aucun
  • .fetch without a default value raises an error if the key is missing. This is useful when a missing key is a bug you want to detect.
  • .fetch(:key, default_value) returns the default value if the key does not exist, without error.

Adding and modifying entries

pikachu = { name: 'Pikachu', type: :electric }

# Add a new key
pikachu[:level] = 25
pikachu[:ability] = :static
p pikachu # => {:name=>"Pikachu", :type=>:electric, :level=>25, :ability=>:static}

# Modify an existing key
pikachu[:level] = 30
puts pikachu[:level] # => 30
  • The syntax is the same for adding and modifying: hash[:key] = value. If the key exists, the value is replaced. Otherwise, it is created.

Checking and deleting

pikachu = { name: 'Pikachu', type: :electric, level: 25 }

# Check for the presence of a key
puts pikachu.key?(:name) # => true
puts pikachu.key?(:ability) # => false

# Size and emptiness
puts pikachu.size # => 3
puts pikachu.empty? # => false
puts {}.empty? # => true

# Delete a key
deleted = pikachu.delete(:level)
puts deleted # => 25 (the deleted value is returned)
p pikachu # => {:name=>"Pikachu", :type=>:electric}
  • .key? checks whether a key exists. This is more reliable than hash[:key] because a key can exist with the value nil.
  • .delete removes the key-value pair and returns the deleted value.

Extracting keys and values

pikachu = { name: 'Pikachu', type: :electric, level: 25 }

p pikachu.keys # => [:name, :type, :level]
p pikachu.values # => ["Pikachu", :electric, 25]
  • .keys returns an Array of all keys. .values returns an Array of all values.

Iterating over a Hash

pikachu = { name: 'Pikachu', type: :electric, level: 25 }

pikachu.each do |key, value|
puts "#{key} : #{value}"
end

Displays:

name : Pikachu
type : electric
level : 25
  • .each passes two values to the block: the key and the value. This is the difference from Arrays, where .each passes only a single element.

You can also iterate over only the keys or only the values:

pikachu.each_key { |key| puts key }
pikachu.each_value { |value| puts value }

Filtering a Hash

Like Arrays, Hashes have .select and .reject:

pokemon = { name: 'Charizard', type: :fire, level: 36, hp: 150 }

# Keep only pairs whose value is an Integer
numbers = pokemon.select { |key, value| value.is_a?(Integer) }
p numbers # => {:level=>36, :hp=>150}
  • .select on a Hash returns a new Hash (unlike .map, which returns an Array).

Merging two Hashes

base = { hp: 78, attack: 84 }
bonus = { attack: 100, speed: 120 }

result = base.merge(bonus)
p result # => {:hp=>78, :attack=>100, :speed=>120}
p base # => {:hp=>78, :attack=>84} (unchanged)
  • .merge returns a new Hash. In case of a shared key, the value from the second Hash wins.
  • The original is not modified.

Nested Hashes

A Hash can contain other Hashes as values. This is very common for modeling complex data:

charizard = {
name: 'Charizard',
types: [:fire, :flying],
stats: { hp: 78, attack: 84, speed: 100 }
}

puts charizard[:stats][:hp] # => 78
puts charizard[:stats][:speed] # => 100
  • You chain [] to access nested levels: hash[:key1][:key2].

The problem arises when an intermediate level does not exist:

# If :stats did not exist, we would get an error
# charizard[:moves][:first] # => Error! NoMethodError (nil has no [])

To avoid this problem, Ruby offers .dig, which navigates deeply without error:

puts charizard.dig(:stats, :hp) # => 78
puts charizard.dig(:moves, :first) # => nil (no error)
  • .dig returns nil if an intermediate level does not exist, instead of raising an error.

Hash with default value

Normally, accessing a nonexistent key returns nil. You can change this behavior with Hash.new:

# Counter: each nonexistent key defaults to 0
counter = Hash.new(0)

counter[:fire] += 1
counter[:fire] += 1
counter[:water] += 1

puts counter[:fire] # => 2
puts counter[:water] # => 1
puts counter[:grass] # => 0 (default value, the key has not been created)
p counter # => {:fire=>2, :water=>1}
  • Hash.new(0) creates a Hash whose default value is 0. When you access a nonexistent key, you get 0 instead of nil.
  • This is very handy for counters: you can do += 1 without checking whether the key already exists.

Conclusion

  • A Hash associates keys with values. Create with { key: value } for Symbol keys.
  • [] returns nil if the key is missing. .fetch raises an error or returns an explicit default.
  • []= adds or modifies an entry. .delete removes a pair.
  • .key? checks for the presence of a key. .keys and .values return Arrays.
  • .each iterates over key-value pairs. .select filters and returns a new Hash.
  • .merge merges two Hashes (the second wins in case of conflict).
  • Nested Hashes model complex data. .dig navigates them without risk of error.
  • Hash.new(default_value) sets the value returned for nonexistent keys.