CrackedRuby logo

CrackedRuby

for Loops

Overview

Ruby implements for loops as syntactic sugar over the each method, providing a familiar iteration construct for developers coming from other languages. The for loop operates on any object that responds to each, including arrays, hashes, ranges, and custom enumerable objects.

The basic for loop syntax follows the pattern for variable in collection. Ruby transforms this into a method call to each on the collection object. Unlike traditional for loops in languages like C or Java, Ruby's for loop does not use numeric indices or increment counters directly.

# Basic for loop over an array
for item in [1, 2, 3, 4, 5]
  puts item
end
# => 1
# => 2
# => 3
# => 4
# => 5

# For loop over a range
for num in 1..3
  puts "Number: #{num}"
end
# => Number: 1
# => Number: 2
# => Number: 3

# For loop over a hash
for key, value in {name: "Alice", age: 30}
  puts "#{key}: #{value}"
end
# => name: Alice
# => age: 30

The for loop creates a new scope for the iteration variable, but unlike block iterators, variables defined inside a for loop remain accessible after the loop completes. This scoping behavior distinguishes for loops from their each method equivalents.

Basic Usage

For loops work with any enumerable collection in Ruby. The most common usage involves arrays, ranges, and hashes. The syntax supports single variables for simple collections and multiple variables for collections that yield multiple values per iteration.

# Iterating over array elements
fruits = ["apple", "banana", "orange"]
for fruit in fruits
  puts "I like #{fruit}"
end
# => I like apple
# => I like banana
# => I like orange

# Working with array indices using each_with_index
for fruit, index in fruits.each_with_index
  puts "#{index}: #{fruit}"
end
# => 0: apple
# => 1: banana
# => 2: orange

# Range iteration with step
for i in (0..10).step(2)
  puts "Even number: #{i}"
end
# => Even number: 0
# => Even number: 2
# => Even number: 4
# => Even number: 6
# => Even number: 8
# => Even number: 10

Hash iteration requires understanding how Ruby handles key-value pairs. When iterating over a hash with a single variable, Ruby assigns an array containing the key-value pair. Using two variables unpacks the key and value automatically.

person = {name: "Bob", city: "New York", profession: "Engineer"}

# Single variable receives [key, value] array
for pair in person
  puts "Pair: #{pair.inspect}"
end
# => Pair: [:name, "Bob"]
# => Pair: [:city, "New York"]
# => Pair: [:profession, "Engineer"]

# Multiple variables unpack key and value
for key, value in person
  puts "The #{key} is #{value}"
end
# => The name is Bob
# => The city is New York
# => The profession is Engineer

String iteration works through the each_char method, though this requires explicit method chaining since strings don't respond to each by default.

# Iterate over string characters
for char in "Hello".each_char
  puts "Character: #{char}"
end
# => Character: H
# => Character: e
# => Character: l
# => Character: l
# => Character: o

Common Pitfalls

Several gotchas arise when using for loops in Ruby, particularly around variable scoping, method dependencies, and performance characteristics. Understanding these issues prevents common mistakes and debugging difficulties.

Variable scoping in for loops differs significantly from block-based iteration. Variables defined inside for loops remain accessible after loop completion, which can lead to unintended variable persistence and namespace pollution.

# Variable scope demonstration
numbers = [1, 2, 3]

# For loop - variable persists
for n in numbers
  doubled = n * 2
  puts doubled
end
puts "doubled variable after loop: #{doubled}" # => 6

# Each block - variable scoped to block
numbers.each do |n|
  tripled = n * 3
  puts tripled
end
# puts tripled  # => NameError: undefined local variable

For loops fail when collections don't respond to the each method. This creates runtime errors that aren't immediately obvious from the syntax.

# Common mistake - trying to iterate over non-enumerable
number = 42
# for digit in number  # => NoMethodError: undefined method `each' for 42:Integer

# Correct approach - convert to enumerable first
for digit in number.to_s.each_char
  puts "Digit: #{digit}"
end
# => Digit: 4
# => Digit: 2

Nested for loops create multiple levels of variable persistence, making variable tracking complex and error-prone.

# Nested loop variable confusion
matrix = [[1, 2], [3, 4], [5, 6]]

for row in matrix
  for cell in row
    processed = cell * 10
    puts processed
  end
  # Both 'row', 'cell', and 'processed' remain accessible here
  puts "Last processed: #{processed}" # Refers to last iteration
end
# All variables still accessible here
puts "Final row: #{row.inspect}"     # => [5, 6]
puts "Final cell: #{cell}"           # => 6
puts "Final processed: #{processed}" # => 60

Type coercion issues arise when for loops receive unexpected data types. Ruby attempts to call each on whatever object follows the in keyword, leading to runtime failures if the object doesn't support iteration.

# Subtle type issue
data = "not_an_array"

# This works - strings respond to each_char
for char in data.each_char
  puts char
end

# This fails - strings don't respond to each directly
# for item in data  # => NoMethodError

Performance & Memory

For loops carry performance implications due to their implementation as method calls to each. The transformation adds method call overhead compared to optimized block-based iteration, though the difference remains minimal for most applications.

Memory usage patterns differ between for loops and block iteration. For loops maintain variables in the outer scope, potentially extending object lifetimes beyond the iteration period. This affects garbage collection timing and memory pressure in long-running processes.

# Memory usage comparison
require 'benchmark'

large_array = (1..100_000).to_a

# For loop timing
time_for = Benchmark.measure do
  total = 0
  for num in large_array
    total += num
  end
end

# Each block timing
time_each = Benchmark.measure do
  total = 0
  large_array.each do |num|
    total += num
  end
end

puts "For loop time: #{time_for.real}"
puts "Each block time: #{time_each.real}"
# Results vary, but each is typically faster

Object allocation patterns show differences in variable lifecycle management. For loops keep iteration variables allocated after completion, while block variables become eligible for garbage collection immediately after block exit.

# Object lifecycle demonstration
def memory_test_for(array)
  for item in array
    processed = item.to_s * 1000  # Large string allocation
  end
  # 'processed' remains in scope, preventing GC
  GC.start
  ObjectSpace.count_objects[:T_STRING]
end

def memory_test_each(array)
  array.each do |item|
    processed = item.to_s * 1000  # Large string allocation
  end
  # 'processed' out of scope, eligible for GC
  GC.start
  ObjectSpace.count_objects[:T_STRING]
end

test_array = [1, 2, 3, 4, 5]
for_strings = memory_test_for(test_array)
each_strings = memory_test_each(test_array)

puts "Strings after for loop: #{for_strings}"
puts "Strings after each block: #{each_strings}"
# Each typically shows fewer allocated strings

Performance optimization strategies focus on minimizing object creation and method calls within loops. For loops benefit from the same optimization techniques as other iteration methods.

# Optimized for loop patterns
data = (1..10_000).to_a

# Avoid repeated method calls
# Inefficient
for item in data
  result = expensive_calculation(item.to_s.upcase.strip)
end

# Efficient
for item in data
  str_item = item.to_s
  upper_item = str_item.upcase
  result = expensive_calculation(upper_item.strip)
end

Reference

Syntax Patterns

Pattern Example Description
for var in collection for x in [1,2,3] Single variable iteration
for var1, var2 in collection for k, v in hash Multiple variable unpacking
for var in collection.method for c in str.each_char Method chaining

Compatible Collection Types

Type Method Required Example Usage
Array each (built-in) for item in [1, 2, 3]
Hash each (built-in) for k, v in {a: 1, b: 2}
Range each (built-in) for i in 1..5
String each_char, each_line for c in "abc".each_char
Enumerator each (built-in) for x in (1..3).each
Custom Objects each implementation for item in custom_enum

Variable Scoping Rules

Context Variable Accessibility Garbage Collection
For loop variable Accessible after loop Delayed until scope exit
Variables defined in loop Accessible after loop Delayed until scope exit
Block local variables Not accessible after block Immediate after block

Equivalent Transformations

For Loop Each Equivalent Notes
for x in arr arr.each {|x| ... } Basic transformation
for x, y in hash hash.each {|x, y| ... } Multiple assignment
for i in (1..n) (1..n).each {|i| ... } Range iteration

Performance Characteristics

Aspect For Loop Each Block Winner
Method call overhead Higher Lower Each
Variable scope management Complex Simple Each
Memory usage Higher (persistent vars) Lower Each
Readability Familiar syntax Ruby idiomatic Each
Debugging Variables persist Clean scope Each

Error Types

Error Cause Solution
NoMethodError: undefined method 'each' Object doesn't implement each Use enumerable or convert object
ArgumentError: wrong number of arguments Variable count mismatch Match variables to yielded values
LocalJumpError Control flow issues Avoid break/next outside loops