Containers, Blocks, and Iterations in Ruby: Arrays, Hashes, Blocks, and Looping Constructs Guide

Containers, Block and Iterations in Ruby

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 eachmap, 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 eachmap, 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 Enumerable methods (map, select, reduce, group_by) over manual loops for anything beyond the most trivial iteration.
  • Use lambdas over procs by default, and reserve Proc.new for cases where the loose arity and return behavior are actually wanted.
  • Use symbols as hash keys unless you have a specific reason to use strings.
  • Build custom classes on top of Enumerable by defining each — it’s a small investment for a large payoff in expressiveness.
  • Avoid for...in; use each, times, or map instead, 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

Total
1
Shares

Leave a Reply

Previous Post
classes, objects and variables in ruby

Classes, Objects, and Variables in Ruby: Object-Oriented Programming Foundations Explained

Next Post
Standard datatypes in Ruby

Standard Data Types in Ruby: Numbers, Strings, Symbols, Booleans, and Nil Explained

Related Posts