Ruby / Contracts: callables, evaluation, failures, and value models / Lesson 8
Build Enumerable pipelines with deliberate evaluation
Use selection, mapping, grouping, aggregation, and lazy enumeration while preserving false values, bounded work, and effect ownership.
On this page
Lesson goals
- Distinguish eager collection transformations from explicit Enumerator::Lazy processing.
- Choose aggregation and filtering according to data meaning rather than shorter syntax.
Enumerable builds on an iteration contract
Enumerable provides collection operations through an object’s each behavior. The yielded values and number of yields matter: a custom iterator can produce unexpected shapes or effects even if it includes the familiar module. Array map with a block evaluates eagerly and returns a result array, while calling map without a block can return an Enumerator. lazy creates a different evaluation pipeline. Do not transfer assumptions from another language’s map method by name alone. Keep predicates and transformations understandable, and document whether traversal can repeat work or observe a changing source.
Group records before applying a clear total rule
The example groups fixed archive records by kind and sums their copy counts. fetch makes the required keys explicit, although it still does not validate each value’s type or positivity. Sorting the final key-value pairs gives deterministic display order rather than relying on unrelated source arrival. group_by retains the grouped elements, so it has memory costs and is not a streaming reducer for an unlimited source. Use a direct accumulation policy when retaining every grouped record is unnecessary. The shown output is reasoned only because no Ruby execution was available.
records = [
{kind: 'book', copies: 2},
{kind: 'map', copies: 4},
{kind: 'book', copies: 1}
]
totals = records.group_by { |record| record.fetch(:kind) }
.transform_values { |group| group.sum { |record| record.fetch(:copies) } }
totals.sort.each { |kind, total| puts "#{kind}:#{total}" }
book:3
map:4
Expected output is book:3 then map:4. The entire supplied array is processed and retained during grouping. No data source, database, or interpreter was contacted.
Filtering can intentionally discard meaningful values
select keeps elements whose predicate is truthy, map transforms every element, and filter_map keeps truthy transformation results. In Ruby that means filter_map drops both nil and false, not only missing values. It is therefore inappropriate when false is a legitimate transformed result that must survive. compact removes nil from an array but does not remove false. Choose the operation from the result contract, and make partial rejection visible when every source record needs an outcome. A convenient pipeline that loses required records can produce a convincing but incomplete success.
Laziness changes when and how much work happens
Enumerator::Lazy defers intermediate processing until a consumer asks for values. take can stop after enough matches instead of processing the whole source. The diagnostic counter below exposes that behavior with a bounded range. Its mutation is teaching instrumentation, not a recommendation to put external effects into reusable transformations. Re-consuming a lazy enumeration can repeat callbacks and may observe changed captured state. An infinite producer still requires a terminating consumer; force or to_a on an unbounded pipeline can attempt unlimited work and memory use.
checked = 0
pipeline = (1..6).lazy.select do |number|
checked += 1
number.even?
end.take(2)
puts checked
puts pipeline.force.join(',')
puts checked
0
2,4
4
Expected output is 0, 2,4, then 4. Only 1 through 4 are checked to obtain two even values. The source was not run or benchmarked.
Measure the relevant work instead of assuming lazy is faster
Lazy pipelines can avoid intermediate arrays or unnecessary traversal, but add their own iteration overhead and do not make global sorting memory-free. For small arrays, a clear eager transformation may be simpler and faster. Choose an actual workload and compare correctness, allocations, elapsed time, and error behavior in an isolated environment when available. Do not return a lazy iterator that depends on a file already closed by the producing method. Source lifetime, cancellation, and repeated consumption remain part of the API contract even when the chain reads like a sequence of pure operations.
Common errors and fixes
False results disappear from a transformed collection.
- Why it happens
- filter_map kept only truthy results.
- How to fix it
- Use map or another explicit policy that preserves required false values.
- Prevent it next time
- Test nil and false separately in transformation cases.
A lazy pipeline appears to do no work.
- Why it happens
- No consumer requested its values.
- How to fix it
- Consume it deliberately or choose an eager operation when immediate work is required.
- Prevent it next time
- Keep effects out of deferred callbacks so timing is explicit.
Grouping an unbounded input exhausts memory.
- Why it happens
- group_by retained all grouped elements.
- How to fix it
- Bound input or use an appropriate streaming accumulation model.
- Prevent it next time
- Review retained state as well as traversal count.
Check your understanding
1. Which operation removes nil while retaining false from an array?
Show worked solution
compact follows that rule, while filter_map drops both false and nil transformation results. Verify that removing nil is actually the product policy; a required field that became nil may deserve an explicit rejection instead of silent deletion. Operation semantics and data completeness must be reviewed together.
2. Why can returning a lazy file-derived iterator be unsafe after a block-owned file closes?
Show worked solution
The iteration may not read until the caller consumes it, after the file’s owner has ended its lifetime. Consume within the owning scope or design an iterator that owns acquisition and closure for its entire traversal. Do not infer resource safety from lazy syntax or from a method returning without an exception.
Key takeaways
- Evaluation strategy and retained state are part of the transformation contract.
- Filtering must preserve the product’s required distinctions and completeness.