Overview
Hash default values determine what Ruby returns when accessing a key that does not exist in a hash. Ruby provides multiple approaches for handling missing keys: returning nil, returning a specific default value, or executing a block to compute the default value.
The Hash class supports default values through three primary mechanisms: the default value accessed via Hash#default
, the default block accessed via Hash#default_proc
, and the Hash#fetch
method with fallback values. These mechanisms control hash behavior when Hash#[]
encounters a missing key.
hash = Hash.new("missing")
hash[:nonexistent]
# => "missing"
hash_with_block = Hash.new { |h, k| h[k] = [] }
hash_with_block[:items] << "first"
hash_with_block[:items]
# => ["first"]
Ruby evaluates default values differently depending on the mechanism used. Static default values return the same object for all missing keys, while default blocks execute each time a missing key is accessed, allowing for computed or unique default values per key.
Basic Usage
Creating a hash with a static default value uses Hash.new
with a single argument. This default value returns for any missing key access through Hash#[]
.
scores = Hash.new(0)
scores[:alice] = 85
scores[:bob]
# => 0
scores[:charlie] += 10
scores[:charlie]
# => 10
The Hash#default=
method changes the default value after hash creation. Setting a new default value affects all subsequent missing key accesses but does not modify existing key-value pairs.
config = {}
config.default = "not configured"
config[:timeout]
# => "not configured"
config[:retries] = 3
config.default = "unknown"
config[:timeout]
# => "unknown"
config[:retries]
# => 3
Default blocks provide computed default values using Hash.new
with a block. The block receives the hash and the missing key as parameters, allowing conditional logic for default value generation.
counters = Hash.new { |hash, key| hash[key] = 0 }
counters[:page_views] += 1
counters[:clicks] += 5
counters
# => {:page_views=>1, :clicks=>5}
grouped_items = Hash.new { |hash, key| hash[key] = [] }
grouped_items[:fruits] << "apple"
grouped_items[:vegetables] << "carrot"
grouped_items
# => {:fruits=>["apple"], :vegetables=>["carrot"]}
The Hash#fetch
method provides another approach for handling missing keys with fallback values or blocks. Unlike default values, fetch
does not modify the hash's default behavior for Hash#[]
operations.
settings = { timeout: 30, retries: 3 }
settings.fetch(:timeout)
# => 30
settings.fetch(:max_connections, 100)
# => 100
settings.fetch(:database_url) { ENV['DATABASE_URL'] || 'localhost' }
# => "localhost"
Advanced Usage
Nested hash structures benefit from recursive default block patterns that create multi-level defaults automatically. This pattern eliminates the need for explicit hash initialization at each level.
nested_hash = Hash.new { |h, k| h[k] = Hash.new { |h2, k2| h2[k2] = [] } }
nested_hash[:users][:active] << "alice"
nested_hash[:users][:inactive] << "bob"
nested_hash[:groups][:admin] << "charlie"
nested_hash
# => {:users=>{:active=>["alice"], :inactive=>["bob"]}, :groups=>{:admin=>["charlie"]}}
# Alternative with deeper nesting
deep_nested = Hash.new do |h, k|
h[k] = Hash.new do |h2, k2|
h2[k2] = Hash.new { |h3, k3| h3[k3] = 0 }
end
end
deep_nested[:stats][:daily][:views] += 100
deep_nested[:stats][:monthly][:clicks] += 50
Proc objects stored as default values create reusable default behavior that can be shared across multiple hashes or modified dynamically.
timestamp_default = proc { |hash, key| hash[key] = Time.now }
log_entries = Hash.new(×tamp_default)
error_tracking = Hash.new(×tamp_default)
log_entries[:first_access]
# => 2024-01-15 10:30:45 UTC
error_tracking[:database_error]
# => 2024-01-15 10:30:46 UTC
# Modifying default behavior
factory_proc = proc do |hash, key|
case key.to_s
when /count/
hash[key] = 0
when /list/
hash[key] = []
when /time/
hash[key] = Time.now
else
hash[key] = nil
end
end
smart_hash = Hash.new(&factory_proc)
smart_hash[:view_count] += 10
smart_hash[:item_list] << "first"
smart_hash[:created_time]
# => 2024-01-15 10:30:47 UTC
Method chaining with default blocks enables sophisticated initialization patterns for complex data structures. These patterns reduce boilerplate code for hash manipulation.
class StatCollector
def initialize
@data = Hash.new { |h, k| h[k] = Hash.new(0) }
end
def increment(category, metric, amount = 1)
@data[category][metric] += amount
self
end
def get_stats
@data.transform_values(&:to_h)
end
end
collector = StatCollector.new
collector.increment(:web, :page_views, 100)
.increment(:web, :unique_visitors, 25)
.increment(:api, :requests, 500)
.increment(:api, :errors, 5)
collector.get_stats
# => {:web=>{:page_views=>100, :unique_visitors=>25}, :api=>{:requests=>500, :errors=>5}}
Custom default value logic can implement caching, lazy loading, or computed values based on key characteristics or external state.
class ConfigurationHash < Hash
def initialize(config_source)
@config_source = config_source
super() do |hash, key|
value = @config_source.fetch_setting(key.to_s)
hash[key] = value if value
value
end
end
end
class MockConfigSource
def fetch_setting(key)
case key
when "timeout" then 30
when "retries" then 3
when "debug" then false
end
end
end
config = ConfigurationHash.new(MockConfigSource.new)
config[:timeout]
# => 30
config.keys
# => [:timeout]
Common Pitfalls
Mutable default values create shared references across all missing key accesses, leading to unexpected modifications affecting multiple keys. This occurs when using objects like arrays, hashes, or custom objects as static defaults.
# PROBLEMATIC: Shared mutable default
groups = Hash.new([])
groups[:admins] << "alice"
groups[:users] << "bob"
groups[:guests]
# => ["alice", "bob"]
# All keys share the same array object
groups[:admins].object_id == groups[:users].object_id
# => true
# CORRECT: Use block for unique defaults
groups = Hash.new { |h, k| h[k] = [] }
groups[:admins] << "alice"
groups[:users] << "bob"
groups[:guests]
# => []
Default blocks that modify the hash during execution can create infinite recursion when the block access triggers itself. This pattern occurs when the default block reads from the same hash with missing keys.
# PROBLEMATIC: Recursive default block
recursive_hash = Hash.new do |h, k|
h[k] = h[:default_value] + "_" + k.to_s
end
recursive_hash[:test]
# => SystemStackError: stack level too deep
# CORRECT: Avoid self-referential access
safe_hash = Hash.new do |h, k|
base_value = "default"
h[k] = base_value + "_" + k.to_s
end
safe_hash[:test]
# => "default_test"
Modifying hash structure during default block execution can cause unexpected behavior, particularly when iterating over the hash or when multiple threads access the hash concurrently.
modification_hash = Hash.new do |h, k|
# Modifying during iteration can cause issues
h.each { |existing_k, v| h.delete(existing_k) if existing_k.to_s.start_with?("temp_") }
h[k] = "value_#{k}"
end
modification_hash[:temp_1] = "temporary"
modification_hash[:permanent] = "keep"
modification_hash[:temp_2]
# => "value_temp_2"
# Side effect: temp_1 was deleted
modification_hash.keys
# => [:permanent, :temp_2]
Default value persistence differs between Hash#[]
access and Hash#fetch
usage. Default blocks only execute for Hash#[]
operations, while fetch
with blocks executes the fetch block instead of the hash's default block.
hash_with_default = Hash.new { |h, k| h[k] = "from_default_block" }
hash_with_default[:missing_key]
# => "from_default_block"
hash_with_default.has_key?(:missing_key)
# => true
hash_with_default.fetch(:another_missing) { "from_fetch_block" }
# => "from_fetch_block"
hash_with_default.has_key?(:another_missing)
# => false
Serialization libraries often ignore default values and default blocks when converting hashes to JSON, YAML, or other formats. This can cause data loss when deserializing hashes that depend on default behavior.
require 'json'
original = Hash.new { |h, k| h[k] = [] }
original[:items] << "test"
# JSON serialization loses default block
json_string = original.to_json
# => "{\"items\":[\"test\"]}"
deserialized = JSON.parse(json_string)
deserialized[:missing]
# => nil (not an empty array)
# Manual restoration required
restored = Hash.new { |h, k| h[k] = [] }
restored.merge!(deserialized)
restored[:missing]
# => []
Reference
Core Methods
Method | Parameters | Returns | Description |
---|---|---|---|
Hash.new |
None | Hash |
Creates hash with nil default |
Hash.new(default) |
default (Object) |
Hash |
Creates hash with static default value |
Hash.new(&block) |
Block `{ | h,k | ... }` |
#default |
None | Object |
Returns current default value |
#default= |
value (Object) |
Object |
Sets default value |
#default_proc |
None | Proc or nil |
Returns default block |
#default_proc= |
proc (Proc) or nil |
Proc or nil |
Sets default block |
#fetch(key) |
key (Object) |
Object |
Returns value or raises KeyError |
#fetch(key, default) |
key (Object), default (Object) |
Object |
Returns value or default |
#fetch(key, &block) |
key (Object), Block |
Object |
Returns value or block result |
Default Block Signatures
Pattern | Block Parameters | Usage |
---|---|---|
`{ | hash, key | ... }` |
`{ | h, k | h[k] = value }` |
`{ | h, k | compute(k) }` |
Behavior Matrix
Access Method | Default Value | Default Block | No Default |
---|---|---|---|
hash[:key] |
Returns default value | Executes block | Returns nil |
hash.fetch(:key) |
Raises KeyError | Raises KeyError | Raises KeyError |
hash.fetch(:key, fallback) |
Returns fallback | Returns fallback | Returns fallback |
hash.fetch(:key) { block } |
Executes fetch block | Executes fetch block | Executes fetch block |
Common Default Patterns
# Static defaults
Hash.new(0) # Numeric counters
Hash.new("") # String accumulation (dangerous with mutation)
Hash.new(false) # Boolean flags
# Block defaults for unique objects
Hash.new { |h, k| h[k] = [] } # Arrays per key
Hash.new { |h, k| h[k] = {} } # Nested hashes
Hash.new { |h, k| h[k] = Set.new } # Sets per key
Hash.new { |h, k| h[k] = 0 } # Counters with storage
# Computed defaults
Hash.new { |h, k| h[k] = Time.now } # Timestamps
Hash.new { |h, k| h[k] = k.to_s.upcase } # Key transformations
Hash.new { |h, k| h[k] = yield_value(k) } # External computation
Error Conditions
Error | Condition | Example |
---|---|---|
KeyError |
fetch without default or block on missing key |
{}.fetch(:missing) |
ArgumentError |
Invalid arguments to Hash.new | Hash.new(1, 2, 3) |
SystemStackError |
Recursive default block | Block calls hash[missing_key] |
FrozenError |
Modifying frozen hash in default block | Default block tries to store on frozen hash |