Skip to content

Hash

What is a Hash?

A Hash is an ordered collection of key-value pairs. Keys map to values — you look up a value by its key. Hashes are Frankie's dictionary type, modelled after Python's dict but with Ruby's do...end iteration syntax.

person  = {name: "Alice", age: 30, active: true}
config  = {host: "localhost", port: 5432, db: "app"}
empty   = {}

Hashes are mutable — you can add, change, and remove keys after creation.


Creating Hashes

Symbol Keys (most common)

user = {name: "Alice", age: 30}

Symbol keys are written without quotes — name: is the key, "Alice" is the value.

String Keys

When keys come from external data (JSON, user input, database results), they are string keys:

row = {"name" => "Alice", "age" => 30}

In practice, both forms work interchangeably for access — Frankie normalises keys internally.

Nested Hashes

config = {
  database: {
    host: "localhost",
    port: 5432,
    credentials: {user: "admin", pass: "secret"}
  },
  cache: {ttl: 300}
}

Accessing Values

h = {name: "Alice", age: 30}

puts h["name"]   # Alice
puts h["age"]    # 30

# Missing key returns nil — no crash
puts h["missing"]   # nil

Safe Nested Access with .dig

config = {database: {host: "localhost", port: 5432}}

puts config.dig("database", "host")     # localhost
puts config.dig("database", "port")     # 5432
puts config.dig("database", "missing")  # nil  — no crash
puts config.dig("nope", "also_nope")    # nil  — no crash

.dig works through any combination of hashes and vectors:

data = {users: [{name: "Alice", score: 95}, {name: "Bob", score: 87}]}
puts data.dig("users", 0, "name")    # Alice
puts data.dig("users", 1, "score")   # 87
puts data.dig("users", 5, "name")    # nil

Fetch with Default

h = {name: "Alice"}

puts h.fetch("name", "unknown")    # Alice
puts h.fetch("age",  0)            # 0  — key missing, returns default

Modifying Hashes

h = {a: 1, b: 2}

# Add or update a key
h["c"] = 3
h["a"] = 99
puts h   # {a: 99, b: 2, c: 3}

# Remove a key
h.delete("b")
puts h   # {a: 99, c: 3}

# Merge — returns new hash, right wins on conflict
puts {a: 1, b: 2} | {b: 3, c: 4}   # {a: 1, b: 3, c: 4}

# Merge in place
h.merge_bang({d: 5, a: 0})
puts h   # {a: 0, c: 3, d: 5}

Hash destructuring

user = {name: "Alice", age: 30, role: "admin"}
{name, age} = user
puts name    Alice
puts age     30

Inspecting Hashes

h = {name: "Alice", age: 30, active: true}

puts h.keys         # [name, age, active]
puts h.values       # [Alice, 30, true]
puts h.size         # 3
puts h.has_key?("name")     # true
puts h.has_key?("missing")  # false

Iterating

.each — key and value

{name: "Alice", age: 30}.each do |k, v|
  puts "#{k}: #{v}"
end
# name: Alice
# age: 30

.each_pair — identical to .each

config.each_pair do |key, val|
  puts "#{key} = #{val}"
end

.map — transform to a vector

.map on a hash yields [key, value] pairs as a vector of vectors:

pairs = {a: 1, b: 2, c: 3}.map do |k, v|
  "#{k}=#{v}"
end
puts pairs   # ["a=1", "b=2", "c=3"]

.map_hash — transform to a new hash

prices = {apple: 1.20, banana: 0.50, cherry: 3.00}

doubled = prices.map_hash do |k, v|
  [k, v * 2]
end
puts doubled   # {apple: 2.4, banana: 1.0, cherry: 6.0}

# Upcase all keys
upper = {a: 1, b: 2}.map_hash do |k, v|
  [k.to_s.upcase, v]
end
puts upper   # {"A": 1, "B": 2}

.select / .reject — filter pairs

scores = {alice: 95, bob: 72, carol: 88, dave: 61}

passing = scores.select do |k, v|
  v >= 80
end
puts passing   # {alice: 95, carol: 88}

failing = scores.reject do |k, v|
  v >= 80
end
puts failing   # {bob: 72, dave: 61}

Converting

h = {x: 1, y: 2, z: 3}

# To a vector of [key, value] pairs
puts h.to_a   # [["x", 1], ["y", 2], ["z", 3]]

# Build a hash from a vector using each_with_object
squares = [1,2,3,4,5].each_with_object({}) do |n, h|
  h[n] = n * n
end
puts squares   # {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}

The | Merge Operator

Merges two hashes. Right-hand keys win on conflict. Chains naturally.

defaults = {color: "blue", size: "md", weight: "normal"}
custom   = {color: "red", size: "lg"}

result = defaults | custom
puts result   # {color: red, size: lg, weight: normal}

# Layered config
base = {debug: false, log: "info", timeout: 30}
env  = {log: "debug"}
local = {timeout: 5}

config = base | env | local
puts config   # {debug: false, log: debug, timeout: 5}

Nested Access Patterns

# Safe navigation with &.
h = {user: {name: "Alice"}}
puts h["user"]&.[]("name")   # Alice — awkward

# Better: use .dig
puts h.dig("user", "name")   # Alice — clean

# Deep default pattern
def get_config(h, *keys)
  result = h.dig(*keys)
  if result == nil then "default" else result end
end
puts get_config(h, "user", "name")   # Alice
puts get_config(h, "user", "age")    # default

Common Patterns

# Frequency count
words = ["apple", "banana", "apple", "cherry", "banana", "apple"]
freq = words.each_with_object({}) do |w, h|
  cur = h[w]
  h[w] = (if cur == nil then 0 else cur end) + 1
end
puts freq   # {apple: 3, banana: 2, cherry: 1}

# Index by field
people = [
  {name: "Alice", id: 1},
  {name: "Bob",   id: 2},
  {name: "Carol", id: 3}
]
by_id = people.each_with_object({}) do |person, h|
  h[person["id"]] = person["name"]
end
puts by_id[2]   # Bob

# Invert a hash
original = {a: 1, b: 2, c: 3}
inverted = original.map_hash do |k, v|
  [v, k.to_s]
end
puts inverted   # {1: "a", 2: "b", 3: "c"}

Quick Reference

Category Method / Operation
Access h[key] .dig(k1, k2, ...) .fetch(key, default)
Inspect .keys .values .size .has_key?(k)
Mutate h[key]= .delete(key) .merge_bang(other)
Merge h1 \| h2 .merge(other)
Iterate .each do \|k,v\| .each_pair do \|k,v\|
Transform .map do \|k,v\| .map_hash do \|k,v\|
Filter .select do \|k,v\| .reject do \|k,v\|
Convert .to_a .keys .values