If there’s one thing that made me fall in love with Ruby early on, it’s how naturally I can express “do this for every item” without wading through boilerplate. Coming from languages where iteration meant hand-rolled index variables and for (int i = 0; ...) loops, Ruby’s blocks and Enumerable methods felt like a different way of thinking entirely. In this article, I want to cover Ruby’s core containers — arrays and hashes — along with blocks, procs, lambdas, and the various looping constructs, and explain not just how to use them but why they’re built the way they are.
Arrays: Ordered, Mixed-Type Collections
A Ruby Array is an ordered, integer-indexed collection that can hold objects of any type, mixed together in the same array:
mixed = [1, "two", :three, 4.0, [5, 6], { seven: 7 }]
puts mixed.inspect
puts mixed.length
Output:
[1, "two", :three, 4.0, [5, 6], {:seven=>7}]
6
Creating and Accessing Arrays
numbers = [10, 20, 30, 40, 50]
puts numbers[0] # 10
puts numbers[-1] # 50, negative indexing from the end
puts numbers[1..3].inspect # [20, 30, 40], inclusive range
puts numbers[1...3].inspect # [20, 30], exclusive range
puts numbers.first(2).inspect
puts numbers.last(2).inspect
Output:
10
50
[20, 30, 40]
[20, 30]
[10, 20]
[40, 50]
I use negative indexing and range slicing constantly — it eliminates a lot of the manual index math I used to write in other languages.
Modifying Arrays
arr = [1, 2, 3]
arr.push(4) # same as arr << 4
arr << 5
arr.unshift(0)
puts arr.inspect
arr.pop
arr.shift
puts arr.inspect
arr.insert(2, 99)
puts arr.inspect
arr.delete(99)
puts arr.inspect
Output:
[1, 2, 3, 4, 5]
[0, 1, 2, 3, 4]
[0, 1, 99, 2, 3, 4]
[0, 1, 2, 3, 4]
Internal Working: How Arrays Store Data
Ruby’s Array is backed by a C-level dynamic array (similar in spirit to a Vector in C++ or ArrayList in Java), not a linked list. This means index-based access (arr[5]) is O(1), while inserting or removing from the front (unshift/shift) is O(n) because every subsequent element has to shift position in memory. I keep this in mind when choosing data structures for performance-sensitive code — if I’m doing a lot of front-insertion, I reconsider whether an array is really the right structure, or whether I should reverse my approach and append/pop from the end instead, which is O(1) amortized.
Ruby also over-allocates capacity behind the scenes (similar to how many dynamic array implementations work), so that repeated push calls don’t require a full reallocation every single time — the array’s backing store grows in chunks, amortizing the cost of growth across many operations.
Hashes: Key-Value Containers
A Hash maps keys to values, and in modern Ruby, hashes maintain insertion order — a guarantee that wasn’t always true in older Ruby versions, and one I rely on more than I probably should.
person = { name: "Aisha", age: 28, city: "Lahore" }
puts person.inspect
puts person[:name]
person.each do |key, value|
puts "#{key}: #{value}"
end
Output:
{:name=>"Aisha", :age=>28, :city=>"Lahore"}
Aisha
name: Aisha
age: 28
city: Lahore
Hash Operations I Use Regularly
h = { a: 1, b: 2 }
h[:c] = 3
puts h.inspect
puts h.key?(:a)
puts h.fetch(:z, "default value")
puts h.merge(d: 4).inspect
puts h.select { |k, v| v > 1 }.inspect
puts h.transform_values { |v| v * 10 }.inspect
puts h.to_a.inspect
Output:
{:a=>1, :b=>2, :c=>3}
true
default value
{:a=>1, :b=>2, :c=>3, :d=>4}
{:b=>2, :c=>3}
{:a=>10, :b=>20, :c=>30}
[[:a, 1], [:b, 2], [:c, 3]]
I specifically want to call out fetch with a default — it’s a much safer habit than h[:missing_key], which silently returns nil and can propagate nil-related bugs downstream. fetch without a default raises KeyError, which I actually prefer for catching typos in key names early.
Internal Working: Hashes Are Hash Tables
Under the hood, a Ruby Hash is a genuine hash table: keys are run through a hash function to compute a bucket, giving average O(1) lookup, insertion, and deletion. The insertion-order guarantee is maintained separately, alongside the hash table structure, so you get both fast lookups and predictable iteration order — a combination that isn’t trivial to implement efficiently, and one of the reasons Ruby’s Hash implementation has been rewritten more than once over the years for performance.
One practical consequence: mutable objects (like a plain String) make risky hash keys, because if you mutate the key object after inserting it, the hash’s internal bucket placement can become inconsistent with the object’s current hash value. This is part of why symbols — immutable and pre-hashed — are the idiomatic default for hash keys in Ruby.
Blocks: Ruby’s Signature Feature
A block is a chunk of code you pass to a method, delimited either by do...end or curly braces { }. This is the feature that makes Ruby’s iteration style so expressive.
[1, 2, 3].each do |n|
puts n * 2
end
[1, 2, 3].each { |n| puts n * 2 }
Output:
2
4
6
2
4
6
My personal convention: { } for single-line blocks, do...end for multi-line blocks. It’s not enforced by the language, but it’s a widely followed community convention that makes code more scannable.
yield: How Methods Receive Blocks
Any Ruby method can accept a block implicitly, and inside the method, yield hands control (and optional values) to that block:
def repeat_three_times
yield 1
yield 2
yield 3
end
repeat_three_times { |n| puts "Iteration #{n}" }
Output:
Iteration 1
Iteration 2
Iteration 3
I can check whether a block was even given, and branch accordingly:
def greet
if block_given?
yield "Hello"
else
puts "No block given"
end
end
greet { |msg| puts "#{msg}, friend!" }
greet
Output:
Hello, friend!
No block given
This is exactly how methods like each, map, and select are implemented internally within Ruby’s own C source — they call yield on each element and let the caller’s block decide what to do with it.
Procs and Lambdas: Blocks as First-Class Objects
Blocks are convenient, but sometimes I want to store a chunk of code in a variable, pass it around, or call it later. That’s where Proc and lambda come in.
square = Proc.new { |n| n * n }
puts square.call(5)
puts square.(5) # alternate call syntax
puts square[5] # yet another alternate call syntax
cube = lambda { |n| n ** 3 }
puts cube.call(3)
triple = ->(n) { n * 3 } # "stabby lambda" syntax
puts triple.call(4)
Output:
25
25
25
27
12
Procs vs Lambdas: The Real Differences
This distinction confused me for a long time, so let me be precise about it, because it genuinely matters in practice.
Argument strictness. Lambdas enforce arity strictly; procs are forgiving:
lax_proc = Proc.new { |a, b| puts "a=#{a}, b=#{b}" }
lax_proc.call(1) # doesn't raise, b is just nil
strict_lambda = lambda { |a, b| puts "a=#{a}, b=#{b}" }
begin
strict_lambda.call(1)
rescue ArgumentError => e
puts "Error: #{e.message}"
end
Output:
a=1, b=
Error: wrong number of arguments (given 1, expected 2)
return behavior. This is the one that actually causes bugs. A return inside a lambda just exits the lambda, like a normal method. A return inside a proc tries to return from the enclosing method, which can raise LocalJumpError if that method has already finished executing.
def test_lambda_return
l = lambda { return 10 }
l.call
puts "This line runs, because lambda's return only exits the lambda"
20
end
def test_proc_return
p = Proc.new { return 10 }
p.call
puts "This line never runs"
20
end
puts test_lambda_return
puts test_proc_return
Output:
This line runs, because lambda's return only exits the lambda
20
10
Because of this difference, I default to lambdas whenever I’m storing reusable logic in a variable, and I only reach for Proc.new when I specifically want the more permissive, block-like behavior.
Enumerable: The Module That Powers Iteration
Almost every collection method you use in Ruby beyond basic each — map, select, reject, reduce, sort_by, group_by, and dozens more — comes from the Enumerable module, which is mixed into Array, Hash, Range, and any custom class that defines each and includes Enumerable.
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
puts numbers.map { |n| n * n }.inspect
puts numbers.select { |n| n.even? }.inspect
puts numbers.reject { |n| n.even? }.inspect
puts numbers.reduce(:+)
puts numbers.reduce(0) { |sum, n| sum + n }
puts numbers.group_by { |n| n % 3 }.inspect
puts numbers.sort_by { |n| -n }.first(3).inspect
puts numbers.partition { |n| n > 5 }.inspect
puts numbers.each_slice(3).to_a.inspect
puts numbers.each_cons(2).to_a.inspect
Output:
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
[2, 4, 6, 8, 10]
[1, 3, 5, 7, 9]
55
55
{1=>[1, 4, 7, 10], 2=>[2, 5, 8], 0=>[3, 6, 9]}
[10, 9, 8]
[[6, 7, 8, 9, 10], [1, 2, 3, 4, 5]]
[[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]]
[[1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7], [7, 8], [8, 9], [9, 10]]
I genuinely reach for Enumerable methods before I write a manual loop in almost every situation. reduce/inject in particular replaced a huge amount of manual accumulator-variable code I used to write.
Building a Custom Enumerable Class
Here’s something I find genuinely elegant about Ruby: I can make my own class fully iterable just by defining each and including Enumerable.
class TeamRoster
include Enumerable
def initialize
@players = []
end
def add(player)
@players << player
self
end
def each
@players.each { |p| yield p }
end
end
roster = TeamRoster.new
roster.add("Zara").add("Bilal").add("Omar")
puts roster.map(&:upcase).inspect
puts roster.select { |p| p.length > 4 }.inspect
puts roster.sort.inspect
puts roster.count
Output:
["ZARA", "BILAL", "OMAR"]
["Zara", "Bilal"]
["Bilal", "Omar", "Zara"]
3
I only had to write each — map, select, sort, and count all came for free from Enumerable. This is a good example of Ruby’s mixin-based design philosophy: define one primitive method, and inherit a whole vocabulary of behavior built on top of it.
Looping Constructs
Beyond each and friends, Ruby has traditional loop constructs, though I use them less often than block-based iteration.
i = 0
while i < 3
puts "while: #{i}"
i += 1
end
i = 0
until i >= 3
puts "until: #{i}"
i += 1
end
3.times { |i| puts "times: #{i}" }
for i in 0..2
puts "for: #{i}"
end
loop do
i += 1
break if i > 5
next if i.even?
puts "loop: #{i}"
end
Output:
while: 0
while: 1
while: 2
until: 0
until: 1
until: 2
times: 0
times: 1
times: 2
for: 0
for: 1
for: 2
loop: 3
loop: 5
A subtlety worth knowing: for...in does not create a new scope for its loop variable — i leaks into the surrounding scope after the loop ends. each with a block, by contrast, keeps the block variable scoped to the block itself. This is one of several reasons the Ruby community strongly favors each/times/map over for loops — I basically never use for in real code anymore, and I’d recommend you don’t either.
for i in 1..3; end
puts i # 3, i still exists here - leaked from the loop
[1, 2, 3].each { |j| }
puts defined?(j) # nil, j does not exist outside the block
Output:
3
Practical, Real-World Example
Here’s a small realistic script that ties containers, blocks, and iteration together — grouping a list of orders by status and computing totals:
orders = [
{ id: 1, status: :shipped, total: 49.99 },
{ id: 2, status: :pending, total: 19.50 },
{ id: 3, status: :shipped, total: 89.00 },
{ id: 4, status: :cancelled, total: 15.00 },
{ id: 5, status: :pending, total: 32.25 }
]
summary = orders.group_by { |order| order[:status] }.transform_values do |group|
{ count: group.size, total: group.sum { |o| o[:total] }.round(2) }
end
summary.each do |status, data|
puts "#{status}: #{data[:count]} orders, $#{data[:total]}"
end
Output:
shipped: 2 orders, $138.99
pending: 2 orders, $51.75
cancelled: 1 orders, $15.0
This is the kind of code I write daily in real applications — no manual loop counters, no mutable accumulator variables scattered around, just composed, declarative transformations.
Common Mistakes I See
Mutating a collection while iterating over it. This produces unpredictable results (skipped elements, IndexErrors). Use select/reject to build a new collection, or each.to_a.each style patterns, rather than deleting elements mid-each.
Using for loops out of old habit. As shown above, it leaks scope and offers no real benefit over each.
Confusing map with each. each returns the original collection unchanged and is for side effects; map returns a new, transformed collection. Using each when you meant map is a very common beginner mistake.
Using mutable objects as hash keys and then mutating them after insertion, which can silently break lookups.
Reaching for Proc.new when a lambda is what’s actually needed, especially when strict argument checking matters.
Best Practices I Follow
- Prefer
Enumerablemethods (map,select,reduce,group_by) over manual loops for anything beyond the most trivial iteration. - Use lambdas over procs by default, and reserve
Proc.newfor cases where the loose arity andreturnbehavior are actually wanted. - Use symbols as hash keys unless you have a specific reason to use strings.
- Build custom classes on top of
Enumerableby definingeach— it’s a small investment for a large payoff in expressiveness. - Avoid
for...in; useeach,times, ormapinstead, for proper variable scoping.
Summary
Ruby’s containers and iteration tools reflect the language’s core philosophy: express intent, not mechanics. Arrays and hashes give you fast, well-understood data structures backed by dynamic arrays and hash tables respectively. Blocks, procs, and lambdas let you treat chunks of behavior as first-class values, with lambdas offering the stricter, more predictable semantics I reach for by default. And the Enumerable module ties it all together, turning a single each method into an entire vocabulary of map, select, reduce, and more — both for built-in collections and for your own custom classes. Once you get comfortable thinking in terms of blocks and enumerable transformations rather than manual loops, your Ruby code naturally becomes shorter, safer, and more expressive.
References
- Ruby Official Documentation — Array
- Ruby Official Documentation — Hash
- Ruby Official Documentation — Enumerable module
- Ruby Official Documentation — Proc class
- Ruby Official Documentation — Control expressions and looping constructs
- RubyGems Guides — Make your own gem