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: valueonly 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,
[]returnsnilwithout 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
.fetchwithout 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 thanhash[:key]because a key can exist with the valuenil..deleteremoves 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]
.keysreturns an Array of all keys..valuesreturns 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
.eachpasses two values to the block: the key and the value. This is the difference from Arrays, where.eachpasses 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}
.selecton 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)
.mergereturns 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)
.digreturnsnilif 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 is0. When you access a nonexistent key, you get0instead ofnil.- This is very handy for counters: you can do
+= 1without checking whether the key already exists.
Conclusion
- A Hash associates keys with values. Create with
{ key: value }for Symbol keys. []returnsnilif the key is missing..fetchraises an error or returns an explicit default.[]=adds or modifies an entry..deleteremoves a pair..key?checks for the presence of a key..keysand.valuesreturn Arrays..eachiterates over key-value pairs..selectfilters and returns a new Hash..mergemerges two Hashes (the second wins in case of conflict).- Nested Hashes model complex data.
.dignavigates them without risk of error. Hash.new(default_value)sets the value returned for nonexistent keys.