This episode dissects Ruby collections: common array and hash operations, hash default values and capacity, Enumerator with next and peek, Enumerator::Lazy for large data, Enumerator.produce, Set, which has been a core class since Ruby 4.0, and Range with cover?.

Collections are where Ruby stores and manipulates data at scale. Array for sequences, Hash for key-value mappings, Range for spans, and Set for unique membership. Understanding when to use which — and how iteration works behind the scenes — determines how efficient your code is.
This episode 10 dissects the four core collections: common Array and Hash operations along with default values, the Enumerator mechanism with next and peek, lazy evaluation for large data, Enumerator.produce, Set which has been a core class since Ruby 4.0, and Range#cover? often used for validation.
Array and Hash provide many transformation methods. Some ! methods change the original data (like sort!), the rest return copies:
angka = [3, 1, 4, 1, 5]
puts angka.sort.inspect
puts angka.first
puts angka.max
puts angka.sum
data = { a: 1, b: 2 }
puts data.key?(:a)
puts data.values.inspectangka.sort returns a sorted array without changing the original, angka.first takes the first element, angka.max the largest value, and angka.sum sums them. On the hash side, data.key?(:a) checks whether a key exists and data.values retrieves all values. All these operations work thanks to the Enumerable module included by Array.
A hash can be given a default value returned when a key isn't found — very useful for counters and accumulators:
penghitung = Hash.new(0)
penghitung[:ruby] += 1
puts penghitung[:ruby]
puts penghitung[:rails]Hash.new(0) creates a hash that returns 0 for keys that don't exist yet. Because of that, penghitung[:ruby] += 1 is safe to do without manual initialization, and penghitung[:rails] returns 0. For large data, Ruby also provides a way to set an initial capacity — a pattern that saves rehashing when the number of keys can be estimated.
A method that accepts a block (like each) can actually be turned into an Enumerator — an object that manages iteration as a manual consumer. With an Enumerator, you pull elements one at a time through next, and look at the next element without consuming it through peek:
enum = [10, 20, 30].each
puts enum.next
puts enum.peek
puts enum.next
puts enum.nextThe code block above displays 10 (the result of next), then 20 (the result of peek, which doesn't consume), then 20 and 30 from the two following next calls. Calling enum.next raises StopIteration after the elements run out — a behavior exploited by the loop construct to iterate to completion.
A regular chain of map and select processes the entire collection at every stage, producing temporary arrays. Lazy evaluation delays processing and pulls only as many elements as requested:
hasil = (1..Float::INFINITY).lazy
.map { |n| n * n }
.select { |n| n.even? }
.first(5)
puts hasil.inspect(1..Float::INFINITY).lazy creates an infinite range that is evaluated lazily. The chain of map and select isn't processed immediately; only until first(5) stops the iteration. This is how you handle infinite sequences or huge datasets without allocating a full array.
Enumerator.produce generates an infinite sequence from an initial value and a transition rule — an elegant pattern for the fibonacci sequence:
fib = Enumerator.produce([0, 1]) { |a, b| [b, a + b] }
puts fib.first(7).map(&:first).inspectEnumerator.produce([0, 1]) { |a, b| [b, a + b] } yields consecutive fibonacci number pairs. fib.first(7).map(&:first) takes the first seven pairs then shows their first elements: [0, 1, 1, 2, 3, 5, 8].
Set is a unique collection without duplicates with O(1) membership lookup. Previously stdlib, Set has been a core class since Ruby 4.0 — no more require "set" needed:
set = Set.new([1, 2, 3])
set.add(4)
puts set.include?(2)
puts set.include?(100)Set.new([1, 2, 3]) builds a set, set.add(4) adds an element, and set.include?(2) checks membership quickly. Because elements must be unique, add with a duplicate value doesn't change the set. For thousands of elements, membership lookup is far faster than with an array.
Range represents a span of values — 1..10 (inclusive) and 1...10 (exclusive). The cover? method checks whether a value is within the range, and is often used for validation:
umur = 25
if umur.between?(17, 60)
puts "Usia produktif"
end
puts (1..10).cover?(15)umur.between?(17, 60) — available thanks to the Comparable module — checks a value within bounds, and (1..10).cover?(15) returns false because 15 is outside the range. Distinguish cover?, which compares bounds, from include?, which iterates elements: for ranges, cover? is far more efficient.
Info
Choose the collection based on your dominant operation: Array for sequences and indexes, Hash for key-value lookup, Set for unique membership, and Range for bound validation. Choosing the right structure affects performance more than writing "smarter" code.
Episode 10 rounds out your collection arsenal: Array and Hash operations with default values, the Enumerator mechanism with next, peek, and Enumerator.produce, lazy evaluation for infinite sequences, Set as a Ruby 4.0 core class, and Range#cover? for validation.
Key takeaways:
Hash.new(0) provides a default value for counters without manual initialization.next and peek..lazy delays processing and only pulls the elements you need.Enumerator.produce generates infinite sequences from an initial value.Range#cover? is more efficient than include? for checking bounds.In the next episode, episode 11, we will discuss concurrency and parallelism — Thread with Mutex and Queue, race conditions, Fiber with cooperative scheduling and Fiber.scheduler, and Ractor for true parallelism with the new Ractor::Port API in Ruby 4.0. This is the most challenging material, but also the most impactful on application performance.