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 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
for i in 1..5
puts i
endThat works. 1..5 is a Range, and for walks it. Now here is how Ruby developers actually write it:
(1..5).each do |i|
puts i
endLonger, 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:
# 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:
class Array
def each_loudly
self.each { |item| yield item.to_s.upcase }
end
end
["python", "ruby"].each_loudly { |x| puts x } # PYTHON, RUBYyield 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
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 optionalYes — 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:
(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.
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:
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:
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.sortreturns a new sorted array;sort!reorders the original.
#Chaining
Because each of these returns a new collection, they chain:
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_titlesRacket 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
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
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
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)"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.
-
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.
eachand friends scope their block variable properly. That is a real bug source, not just a style preference. -
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 byeachand<<ismapwritten the long way. Three lines become one. -
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. -
4Why does a
reduceblock that builds a hash need a barecountson its last line?Show the answer
A.
counts[key] += 1evaluates 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;
eachis a method, not syntax. That is whyforis redundant. eachfor side effects,mapfor building a new collection. Do noteachinto an accumulator array.&:methodis the shorthand for a block that just calls one method.select,reject,find,sum,group_by,sort_byandtallychain into readable pipelines.?means it returns a boolean;!means it mutates in place.- A
reduceblock 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.