Skip to content

Loops

Frankie has four loop constructs and a rich set of iterator methods. The loop constructs handle index-based and condition-based repetition; the iterators handle collection traversal.


while

Runs as long as the condition is truthy.

x = 0
while x < 5
  puts x
  x += 1
end
# 0 1 2 3 4

until

Runs as long as the condition is falsy — the inverse of while.

x = 0
until x == 5
  puts x
  x += 1
end
# 0 1 2 3 4

do...while

Runs the body at least once, then checks the condition. Borrowed from Fortran.

x = 10
do
  puts x
  x += 1
while x < 5
# prints 10 — body ran once even though condition was already false

Useful when you need to guarantee at least one execution — user input loops, retry logic:

do
  name = input("Enter a positive number: ")
  n = name.to_i
while n <= 0

puts "You entered: #{n}"

for...in

Iterates over a range or a vector.

# Over a range
for i in 1..5
  puts i
end
# 1 2 3 4 5

# Exclusive range
for i in 0...4
  puts i
end
# 0 1 2 3

# Over a vector
for item in ["apple", "banana", "cherry"]
  puts item
end

# Over a hash — gives [key, value] pairs
config = {host: "localhost", port: 5432}
config.each do |k, v|
  puts "#{k}: #{v}"
end

n.times

Repeat a block exactly n times. The loop variable counts from 0 to n-1.

3.times do |i|
  puts "iteration #{i}"
end
# iteration 0
# iteration 1
# iteration 2

# Without the variable
5.times do
  print "* "
end
# * * * * *

The standalone times(n) form is identical:

times(3) do |i|
  puts i
end

Loop Control

next

Skip to next iteration

Equivalent to continue in other languages.

[1, 2, 3, 4, 5, 6].each do |n|
  next if n % 2 == 0
  puts n
end
# 1 3 5

break

Exit the loop early

[1, 2, 3, 4, 5].each do |n|
  break if n > 3
  puts n
end
# 1 2 3

break with a value

A break can carry a value out of the loop:

result = [1, 2, 3, 4, 5].each do |n|
  break n * 10 if n == 3
end
puts result   # 30

Postfix forms

next if condition
break if condition

Iterator Methods

For collection traversal, iterator methods with blocks are more idiomatic than for loops. They compose, chain, and express intent more clearly.

.each — iterate for side effects

[1, 2, 3].each do |x|
  puts x * 2
end

{name: "Alice", age: 30}.each do |k, v|
  puts "#{k}: #{v}"
end

.each_with_index — iterate with position

["a", "b", "c"].each_with_index do |val, i|
  puts "#{i}: #{val}"
end
# 0: a
# 1: b
# 2: c

.each_with_object — iterate with accumulator

result = [1, 2, 3, 4, 5].each_with_object({}) do |x, h|
  h[x] = x * x
end
puts result   # {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}

.each_slice — iterate in fixed-size chunks

[1, 2, 3, 4, 5, 6].each_slice(2) do |slice|
  puts slice
end
# [1, 2]
# [3, 4]
# [5, 6]

.each_cons — iterate with sliding window

[1, 2, 3, 4, 5].each_cons(3) do |window|
  puts window
end
# [1, 2, 3]
# [2, 3, 4]
# [3, 4, 5]

.times — iterate n times (method form)

5.times do |i|
  puts i
end

for vs Iterators

Both work. Iterators are generally preferred for collection traversal because they compose and chain cleanly:

# for loop
for n in [1, 2, 3, 4, 5]
  puts n if n > 3
end

# Iterator — composes with select
[1, 2, 3, 4, 5].select do |n|
  n > 3
end.each do |n|
  puts n
end

Use for when iterating over a range by index. Use iterators when transforming or filtering a collection.


Infinite Loops

A while true loop runs forever until break:

count = 0
while true
  count += 1
  break if count >= 5
end
puts count   # 5

Nested Loops

[1, 2, 3].each do |x|
  [10, 20].each do |y|
    puts "#{x} × #{y} = #{x * y}"
  end
end
# 1 × 10 = 10
# 1 × 20 = 20
# 2 × 10 = 20
# ...

# Or use .product for cartesian iteration
[1, 2, 3].product([10, 20]).each do |pair|
  puts "#{pair[0]} × #{pair[1]} = #{pair[0] * pair[1]}"
end

Labeled Loops (v1.22)

A bare break or next only ever affects the loop it's directly inside — for a deeply nested search, that usually means booleans and extra checks to unwind more than one level. while, until, for, and loop can carry a :name label right after the keyword, and break :name / next :name target that exact loop from anywhere nested inside it, even through other unlabeled loops in between.

loop :outer do
  i = 0
  loop do
    i += 1
    break :outer if i > 2
    puts "inner i=#{i}"
  end
  puts "never reached — the labeled break already exited :outer"
end
puts "after outer loop"
# inner i=1
# inner i=2
# after outer loop

next :name works the same way, jumping straight to the next iteration of the labeled loop instead of just the innermost one:

i = 0
while :outer i < 3
  i += 1
  j = 0
  while j < 3
    j += 1
    next :outer if j == 2
    puts "i=#{i} j=#{j}"
  end
end
# i=1 j=1
# i=2 j=1
# i=3 j=1

for takes its label right after for, before the loop variable:

for :outer x in [1, 2, 3]
  for y in [1, 2, 3]
    break :outer if x == 2 and y == 2
    puts "x=#{x} y=#{y}"
  end
end

An unlabeled break/next between the label and the target loop is transparent — it isn't required to also carry the label, and it doesn't accidentally get caught early. Only a loop can be labeled; plain break/next inside iterator blocks (.each, .map, ...) are unaffected.