Hashes: values with names
Key and value pairs, symbols as keys, fetch with a default, and the counting pattern that hashes were made for.
A lookup table
A hash maps keys to values: { name: 'Ada', score: 95 }. This is a shorthand that turns the symbol :name into the key, and it is the syntax you will see everywhere. The full form { :name => 'Ada' } is the same hash with the arrow written out — worth recognising in older code, rarely worth typing.
Reading a value uses square brackets and the key, exactly like an array: student[:name]. Writing one is the same with an equals: student[:passed] = true. A key that has never been set reads back as nil — quiet, but it means student[:typo] does not fail, which is why fetch exists: student.fetch(:name) raises on a missing key, and student.fetch(:missing, 'a default') supplies one.
Walking a hash
each over a hash hands the block two arguments, key and value — student.each do |key, value| — and the order is the order the keys were inserted, which Ruby has guaranteed since version 1.9. keys and values give those collections on their own, and key? asks whether a key exists without caring what it points to.
merge combines two hashes, with the second winning on conflicts, and transform_values rebuilds a hash with every value passed through a block while the keys stay put. Both return new hashes rather than changing the old one.
The counting pattern
Hashes are the natural home for counts, tallies and indexes because of one idiom: ask whether a key exists, start it at zero if not, then add. In Ruby that reads as a short loop, and by the next chapter it will be one line. The same shape groups things: the key is the group, the value is what belongs to it.
That is the moment a hash stops being a record and becomes a tool. When a problem is "how many of each" or "which ones belong together", the answer usually starts with an empty hash and two lines inside a loop.
Keys are compared, not converted
A hash finds a value by hashing its key, so two keys are the same only when they are the same kind of object with the same contents. The symbol :name and the string 'name' are different keys even though they print similarly — which is the first thing that goes wrong when data arrives from JSON, where every key is a string.
Reading a missing key gives nil unless the hash was built with a default: Hash.new(0) answers zero, and Hash.new { |h, k| h[k] = [] } builds an entry on demand. fetch is the other direction — it refuses to guess, raising unless you supply the fallback yourself. The order of a hash is the order keys were first inserted, a guarantee worth relying on.
A hash is a record and a tool
The same structure serves two moods. As a record, the keys are fixed and known: a student has a name, a grade and a score. As a tool, the keys arrive from the data: a tally of words, a grouping of orders by customer. The record is read with fetch because a missing field is a bug; the tool is read with [] or a default because a missing key is an ordinary first sighting.
Knowing which mood you are in decides the method. A record that silently answers nil hides a broken import; a tally that raises on every new word is unusable. The same hash class supports both, and the choice is yours at the call site.
Insertion order is a feature
Ruby hashes remember the order keys were first added, and iteration follows it. That makes a hash double as an ordered report: count the words, then print the counts in the order they appeared. When alphabetical order is wanted, sort the keys explicitly — the hash will not do it for you.
Hash.new([]) is the trap worth knowing: the same array is the default for every missing key, so appending to one key's value appears to fill them all. Use the block form instead, which builds a fresh value per key. And never add or delete keys while iterating a hash — collect the keys first, then change it. Finally, string and symbol keys do not meet: data['name'] and data[:name] are two different entries.
Records, lookups, and counts
student = { name: 'Ada', grade: 'A', score: 95 }
puts student[:name]
puts student[:grade]
student[:passed] = true
p student
puts student.key?(:name)
puts student.fetch(:missing, 'unknown')
student.each do |key, value|
puts "#{key}: #{value}"
end
words = %w[ruby rails gem ruby gem ruby]
counts = {}
words.each do |word|
counts[word] = 0 unless counts.key?(word)
counts[word] = counts[word] + 1
end
p counts
p counts.keys
p counts.values
p counts.max_by { |word, count| count }
p counts.transform_values { |n| n * 2 }
p counts.merge(rails: 1)A gradebook
Run it and read the tallies. Then add a second student, and a loop that prints the class average from the hashes rather than from a separate list.
A hash that grows from arbitrary input never gets nil-checked on read the way a fixed record does. When a key comes from data rather than from your own code, fetch with a default is the difference between a clear answer and a nil that surfaces three lines later.
Try these next
- Build a hash of three prices, then print the total of a basket that refers to them by key.
- Count the letters in a word with
Hash.new(0), and print the counts in insertion order. - Ask a hash for a missing key with
[]and withfetch, and compare what each does.
A basket with a total
Run it and check each line of the receipt. Then change a quantity, add an item that is not in the price list, and decide whether the program should skip it or refuse it.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
What does hash.fetch(:missing, 'none') do?
