Ruby For Loops and Iterators

Blocks, each, map, select, times and upto — and why an experienced Ruby developer will almost never write a for loop.

Ruby Beginner 14 min read Watch the video (10:35)

Ruby has a for loop. You will almost never see one in real Ruby code, and by the end of this lesson you will understand why that is a feature rather than an oddity.

#The for loop exists

ruby
for i in 1..5
  puts i
end

That works. 1..5 is a Range, and for walks it. Now here is how Ruby developers actually write it:

ruby
(1..5).each do |i|
  puts i
end

Longer, and better. The reason is the thing in the vertical bars.

#Blocks — the idea everything is built on

A block is a chunk of code you hand to a method. The method decides when and how often to run it, and what to pass in.

Two ways to write one, and the convention is firm:

ruby
# do...end for multiple lines
[1, 2, 3].each do |n|
  doubled = n * 2
  puts doubled
end

# braces for a single line
[1, 2, 3].each { |n| puts n * 2 }

each is not special syntax. It is a method on Array that runs your block once per element, handing the element in as n. You could write your own:

ruby
class Array
  def each_loudly
    self.each { |item| yield item.to_s.upcase }
  end
end

["python", "ruby"].each_loudly { |x| puts x }   # PYTHON, RUBY

yield calls the block that was passed in. Once you see that iteration is just methods calling blocks, the whole language opens up — and it explains why for is redundant. Everything for can do, a method can do better, because a method can also decide to run the block zero times, twice, in reverse, or in parallel.

#The counting iterators

ruby
5.times { |i| puts "Iteration #{i}" }        # 0,1,2,3,4

1.upto(5) { |i| print i }                    # 12345
5.downto(1) { |i| print i }                  # 54321
1.step(10, 3) { |i| print "#{i} " }          # 1 4 7 10

3.times { puts "Ruby is great" }             # block variable is optional

Yes — 5.times. Integers are objects and times is a method on them. Ruby has no primitives; everything is an object, including nil and true.

Ranges cover the rest:

ruby
(1..5).to_a       # [1, 2, 3, 4, 5]   two dots include the end
(1...5).to_a      # [1, 2, 3, 4]      three dots exclude it
('a'..'e').to_a   # ["a", "b", "c", "d", "e"]

#each versus map — the distinction that matters most

This is the single most important thing in this lesson.

each is for doing. map is for making.

ruby
languages = ["python", "ruby", "php"]

# each returns the ORIGINAL array. Use it for side effects.
languages.each { |lang| puts lang.capitalize }
# => ["python", "ruby", "php"]

# map returns a NEW array of whatever the block returned.
shouted = languages.map { |lang| lang.upcase }
# => ["PYTHON", "RUBY", "PHP"]

If you find yourself writing this:

ruby
result = []
languages.each { |lang| result << lang.upcase }

…that is map, written the long way. Three lines become one.

#The Enumerable toolkit

These methods are available on arrays, hashes, ranges, and anything else that defines each. This handful covers most day-to-day work:

ruby
nums = [4, 8, 15, 16, 23, 42]

nums.select { |n| n.even? }      # [4, 8, 16, 42]      keep matches
nums.reject { |n| n.even? }      # [15, 23]            drop matches
nums.find   { |n| n > 10 }       # 15                  first match only
nums.map    { |n| n * 2 }        # [8, 16, 30, ...]    transform each
nums.sum                         # 108
nums.min                         # 4
nums.max                         # 42
nums.sort_by { |n| -n }          # descending
nums.count  { |n| n > 10 }       # 4
nums.any?   { |n| n > 40 }       # true
nums.all?   { |n| n > 0 }        # true
nums.none?  { |n| n > 100 }      # true
nums.each_slice(2).to_a          # [[4,8],[15,16],[23,42]]
nums.each_with_index { |n, i| puts "#{i}: #{n}" }
nums.group_by { |n| n.even? ? :even : :odd }
# => {even: [4, 8, 16, 42], odd: [15, 23]}

Two Ruby conventions on display there:

  • A method ending in ? returns a boolean. even?, nil?, empty?, include?.
  • A method ending in ! modifies the receiver in place, and usually the non-! version is what you want. sort returns a new sorted array; sort! reorders the original.

#Chaining

Because each of these returns a new collection, they chain:

ruby
videos = [
  { title: "Intro to Python",  views: 197, track: "python" },
  { title: "Python Loops",     views: 381, track: "python" },
  { title: "PHP Web Form",     views: 259, track: "php" },
  { title: "Racket Install",   views: 696, track: "racket" },
]

popular_titles = videos
  .select { |v| v[:views] > 200 }
  .sort_by { |v| -v[:views] }
  .map { |v| "#{v[:title]} (#{v[:views]})" }

puts popular_titles
text
Racket Install (696)
Python Loops (381)
PHP Web Form (259)

Filter, sort, transform. Read top to bottom, each step obvious. That readability is the whole reason people like Ruby.

#reduce — collapse a collection to one value

ruby
nums.reduce(0) { |total, n| total + n }        # 108
nums.reduce { |a, b| a > b ? a : b }           # 42
nums.reduce(:+)                                # 108 — symbol shorthand

# The genuinely useful case: building a hash
videos.reduce(Hash.new(0)) do |counts, video|
  counts[video[:track]] += 1
  counts
end
# => {"python" => 2, "php" => 1, "racket" => 1}

The block receives an accumulator and the current item, and whatever it returns becomes the next accumulator. Hash.new(0) creates a hash where any missing key defaults to 0, so += 1 works without checking first.

#Hashes iterate in pairs

ruby
lessons = { python: 4, php: 5, ruby: 1, racket: 2 }

lessons.each do |track, count|
  puts "#{track}: #{count} lessons"
end

lessons.select { |_, count| count > 2 }      # {python: 4, php: 5}
lessons.map { |track, count| "#{track} (#{count})" }
lessons.sum { |_, count| count }             # 12
lessons.max_by { |_, count| count }          # [:php, 5]

The underscore is a conventional name for "I have to accept this parameter but I am not using it". It signals intent to the next reader.

#Put it together

ruby
CATALOGUE = [
  { title: "Intro to Python",   track: "python", seconds: 647,  views: 197 },
  { title: "Python Basics",     track: "python", seconds: 898,  views: 191 },
  { title: "Python Loops",      track: "python", seconds: 1355, views: 381 },
  { title: "PHP Web Form",      track: "php",    seconds: 266,  views: 259 },
  { title: "Ruby Iterators",    track: "ruby",   seconds: 635,  views: 1000 },
].freeze

def format_duration(seconds)
  format("%d:%02d", seconds / 60, seconds % 60)
end

CATALOGUE.group_by { |v| v[:track] }.each do |track, videos|
  total = videos.sum { |v| v[:seconds] }

  puts "\n#{track.upcase} — #{videos.count} lessons, #{format_duration(total)}"
  puts "-" * 40

  videos.sort_by { |v| -v[:views] }.each_with_index do |video, i|
    puts format("  %d. %-20s %6s  %5d views",
                i + 1, video[:title], format_duration(video[:seconds]), video[:views])
  end
end

top = CATALOGUE.max_by { |v| v[:views] }
puts "\nMost watched: #{top[:title]} (#{top[:views]} views)"
text
PYTHON — 3 lessons, 48:20
----------------------------------------
  1. Python Loops         22:35    381 views
  2. Intro to Python      10:47    197 views
  3. Python Basics        14:58    191 views

PHP — 1 lessons, 4:26
----------------------------------------
  1. PHP Web Form          4:26    259 views

RUBY — 1 lessons, 10:35
----------------------------------------
  1. Ruby Iterators       10:35   1000 views

Most watched: Ruby Iterators (1000 views)

format (an alias of sprintf) uses the same specifiers as C: %d for integers, %02d for zero-padded, %-20s for a left-aligned 20-character string. It is how you get columns that line up.

.freeze on the constant prevents accidental modification — good practice for any constant collection.

Check yourself

4 questions on what you just read. No score is kept — it is only here to catch the bits that did not stick.

  1. 1Why do experienced Ruby developers avoid for?

    Show the answer

    A. It does not create a new scope, so the loop variable leaks out afterwards.

    each and friends scope their block variable properly. That is a real bug source, not just a style preference.

  2. 2You are building a new array from an existing one. Which method?

    Show the answer

    A. map — it returns a new array of whatever the block returned.

    result = [] followed by each and << is map written the long way. Three lines become one.

  3. 3What does .map(&:upcase) mean?

    Show the answer

    A. Shorthand for .map { |x| x.upcase }.

    The &: form works with any no-argument method — &:to_i, &:strip, &:even?. Concise and universally understood.

  4. 4Why does a reduce block that builds a hash need a bare counts on its last line?

    Show the answer

    A. counts[key] += 1 evaluates to the count, so without it the next iteration receives an integer.

    The block's return value becomes the next accumulator. It looks redundant right up until you remove it.

#Key takeaways

  • Blocks are code passed to methods; each is a method, not syntax. That is why for is redundant.
  • each for side effects, map for building a new collection. Do not each into an accumulator array.
  • &:method is the shorthand for a block that just calls one method.
  • select, reject, find, sum, group_by, sort_by and tally chain into readable pipelines.
  • ? means it returns a boolean; ! means it mutates in place.
  • A reduce block must return the accumulator, every iteration.

Finished this one?

The challenge above has no posted solution — that is deliberate. Get it working, then come back for the next lesson.

More tutorials Subscribe