Ruby / Foundations: values, methods, text, and valid objects / Lesson 5
Use arrays and hashes without hiding missing data
Choose positional or key lookup, fetch semantics, symbol/string keys, independent defaults, and deliberate shallow copies.
On this page
Lesson goals
- Distinguish missing values from present nil or false in collection access.
- Avoid shared mutable default objects and explain copy depth.
Select a relationship rather than a convenient literal
Array is an ordered, integer-indexed sequence that can contain objects of different classes and repeated values. Hash maps unique keys to values and preserves its documented traversal order. Choose an array for sequence meaning and a hash for association meaning. Neither structure validates the type or domain of its contents automatically. Symbol keys and String keys are different, so a decoded JSON object and a hand-written symbol-keyed options hash can behave differently despite similar-looking displays. Normalize keys only through a deliberate boundary policy rather than trying every possible spelling throughout application code.
Use fetch when missing input must be explicit
Array bracket lookup and ordinary Hash lookup can return nil for missing positions or keys. If nil is itself a permitted stored value, that result alone cannot identify absence. Hash key? answers presence, while fetch raises on a missing key or uses the supplied default or block. A fetch block computes its fallback only when needed, unlike an argument expression evaluated before the call. Preserve false as a meaningful value instead of using || to replace it unintentionally. The example separates a present false, a missing symbol key, and a distinct String key.
settings = {enabled: false, 'name' => 'Archive'}
puts settings.fetch(:enabled)
puts settings.fetch(:name, 'missing symbol')
puts settings.fetch('name')
puts settings.key?(:enabled)
puts settings.key?(:unknown)
false
missing symbol
Archive
true
false
Expected output is false, missing symbol, Archive, true, false. The symbol :name does not identify the String key name. The source was not run.
A default object can accidentally become shared state
Hash.new([]) returns the same default array for missing keys and does not automatically store a distinct array for each key. Mutating that result can affect every missing lookup. Likewise, Array.new(3, mutable_object) repeats one reference rather than creating three independent objects. Use a block to build distinct values when that is the intended contract. For a hash of groups, the default block can assign a newly created array to the missing key. A default that mutates the hash is deliberate behavior and should be documented when reads may create entries.
Give each group its own container
The default block below creates an array at first access for each category. Records assigned to A and B therefore stay in separate containers. The example is local bookkeeping with synthetic labels, not a database grouping or a concurrent map. If several threads access the same hash, this construction does not provide a combined thread-safe initialization protocol. Keep ownership explicit and use the relevant synchronization boundary when sharing state. A shallow hash or array copy also retains nested object references, so independent outer containers alone do not guarantee independent mutable record contents.
groups = Hash.new { |hash, key| hash[key] = [] }
groups['A'] << 'record-1'
groups['B'] << 'record-2'
puts groups['A'].join(',')
puts groups['B'].join(',')
puts groups['A'].equal?(groups['B'])
record-1
record-2
false
Expected output is record-1, record-2, false. The two keys have different array objects. No Ruby runtime or concurrency test was executed.
Keep collection edits and missing-data policy visible
Deleting or inserting while traversing can have method-specific restrictions and surprising results; prefer constructing a result or using a documented transformation. Some bang methods return nil when no change occurs, so do not assume every filtering call returns an array. A missing item should become an explicit rejection or optional result rather than a fabricated identifier. Test empty, missing, duplicate-key, false, nil, and nested-mutation cases in a compatible isolated project. This course describes those checks but records no Ruby collection execution, stored data migration, or operational recovery.
Common errors and fixes
All missing hash keys appear to share one growing array.
- Why it happens
- A mutable object was supplied as a shared default.
- How to fix it
- Create and assign a separate container in a default block.
- Prevent it next time
- Test two missing keys and compare their container identities.
A symbol lookup misses a String-keyed field.
- Why it happens
- Key representation changed across the input boundary.
- How to fix it
- Validate and translate the key schema once at the boundary.
- Prevent it next time
- Keep internal options and external decoded records explicitly distinguished.
A false setting is treated as missing.
- Why it happens
- A truthiness fallback replaced a legitimate stored value.
- How to fix it
- Use presence or fetch according to the declared contract.
- Prevent it next time
- Include false and nil independently in lookup checks.
Check your understanding
1. Why does duplicating a hash not automatically isolate its nested arrays?
Show worked solution
A normal shallow copy creates another outer container while retaining references to its values. Mutating a nested array can remain visible through both hashes. Define the required depth, preserve meaningful identity, and choose immutable values or an explicit copy policy. Freezing only the copied hash does not solve element-level sharing.
2. When is fetch preferable to bracket lookup for a required archive field?
Show worked solution
When absence should produce a clear failure or explicitly computed fallback instead of silently returning nil. Still validate the fetched valueโs type and domain: a present nil or malformed string can satisfy key presence while violating the requirement. Decide whether missing input is ordinary validation or caller misuse before translating the failure.
Key takeaways
- Presence, value, key type, and default behavior are separate lookup decisions.
- Independent containers can still share mutable nested values.