Control Flow and Methods

Article · 11:00

Ruby control flow reads close to English, and almost all of it produces a value you can assign. Conditions as expressions, guard clauses that let the happy path stay unindented, and what the question mark and bang on a method name promise.

This lesson has 6 runnable examples. Press Try yourself! under any of them to open it in the editor.

Control Flow and Methods

Ruby's control flow reads close to English, and almost all of it produces a value you can assign.

RB Example 1 10 lines
scores = [92, 78, 85, 41, 67]

scores.each do |score|
  grade = if score >= 90 then "A"
          elsif score >= 80 then "B"
          elsif score >= 60 then "C"
          else "F"
          end
  puts format("%3d -> %s", score, grade)
end

Open this example in the editor and change it. Nothing you type is saved.

Note that if returned a value. So did the whole expression. That is why Ruby code rarely assigns the same variable in four branches.

unless, and the modifier form

unless is if not, and reads better when the negative is the natural way to say it. Any of these can be written after the statement they guard.

RB Example 2 12 lines
stock = 0
puts "out of stock" unless stock.positive?

def withdraw(balance, amount)
  return "amount must be positive" unless amount.positive?
  return "insufficient funds" if amount > balance
  "withdrew #{amount}, #{balance - amount} left"
end

puts withdraw(100, 30)
puts withdraw(100, 300)
puts withdraw(100, -5)

Open this example in the editor and change it. Nothing you type is saved.

Those early returns are called guard clauses. They handle the wrong cases first and let the real work sit unindented at the bottom.

case, which compares with ===

case does not use ==. It uses ===, which each class defines for itself: a Range asks "do you fall inside me", a Class asks "are you an instance of me", a Regexp asks "do you match".

RB Example 3 15 lines
def describe(value)
  case value
  when 0 then "zero"
  when 1..9 then "single digit"
  when Integer then "a bigger integer"
  when /\A[a-z]+\z/ then "lowercase word"
  when String then "some other string"
  when nil then "nothing"
  else "a #{value.class}"
  end
end

[0, 5, 1000, "hello", "Hello!", nil, 3.5].each do |v|
  puts format("%-10s %s", v.inspect, describe(v))
end

Open this example in the editor and change it. Nothing you type is saved.

Loops, and why you will rarely write one

RB Example 4 22 lines
3.times { |i| print i, " " }
puts

1.upto(3) { |i| print i, " " }
puts

(1..10).step(3) { |i| print i, " " }
puts

# while and until exist, and are the right tool when the end condition is not
# a collection running out.
countdown = 3
until countdown.zero?
  puts "t-minus #{countdown}"
  countdown -= 1
end

# loop rescues StopIteration for you, which makes it the partner of an
# external iterator.
numbers = [10, 20, 30].each
loop { puts numbers.next }
puts "done"

Open this example in the editor and change it. Nothing you type is saved.

Methods

A method returns its last expression. return is for leaving early.

RB Example 5 25 lines
def area(width, height)
  width * height
end

# Default arguments, and keyword arguments that are required unless defaulted.
def introduce(name, greeting: "Hello", punctuation: "!")
  "#{greeting}, #{name}#{punctuation}"
end

puts area(3, 4)
puts introduce("Aisha")
puts introduce("Kenji", greeting: "Welcome", punctuation: ".")

# Keyword arguments are worth it the moment a call has a bare true in it:
# resize(image, 800, 600, true, false) tells the reader nothing.
def resize(width:, height:, preserve_ratio: true)
  "#{width}x#{height} ratio=#{preserve_ratio}"
end
puts resize(width: 800, height: 600, preserve_ratio: false)

# Splat collects the rest; double splat collects keywords.
def log(level, *messages, **context)
  "[#{level}] #{messages.join(' ')} #{context.inspect}"
end
puts log(:warn, "disk", "nearly full", device: "/dev/sda", pct: 91)

Open this example in the editor and change it. Nothing you type is saved.

The naming conventions carry meaning

Two suffixes are conventions the whole ecosystem follows.

RB Example 6 15 lines
words = ["b", "a", "c"]

sorted = words.sort      # returns a new array
puts sorted.inspect
puts words.inspect       # unchanged

words.sort!              # ! means "this one mutates the receiver"
puts words.inspect

puts words.empty?        # ? means "this returns true or false"
puts words.frozen?

# A one-line method can be written with =, which reads well for short ones.
def double(n) = n * 2
puts double(21)

Open this example in the editor and change it. Nothing you type is saved.

The ! does not mean "dangerous" on its own - it means "the surprising one of a pair". sort! exists because sort exists.

What to take away

  • if, case and begin are expressions; they return values.
  • Guard clauses with unless/if modifiers keep the happy path unindented.
  • case uses ===, so ranges, classes and regexes all work as conditions.
  • Keyword arguments make a call site readable; use them instead of flags.
  • ? means it answers a question, ! means it is the mutating twin.