Skip to content
academia.sh

Lesson 04 / 12

Control Flow

The case expression sends the === message to each when branch, while if and while are syntax that sends no message; statement modifiers and the value of if; the responder of case is the class of the branch object.

Published Updated

In this lesson

  • Shows with an owner measurement that case sends the === message to each when branch
  • Separates if and while as syntax rather than messages by using defined?
  • Finds the responder of case as the class of the branch object, and shows that statement modifiers and if produce a value
Contents

The previous lesson measured that assignment is not a message but the language’s own spelling, and it left a question: of if, while, and case, which ones work with the language’s own spelling, and which one really sends a message? The concepts of conditions and loops were introduced in the Programming Fundamentals course (M01/K02); they are not repeated here. This lesson measures only one question: when a case branch is chosen, which message goes to which object?

In the Rust course (M10/K01), the question “does this construct produce a value” was measured. In Ruby, nearly every control structure produces a value, and that question returns in this lesson only as a side observation. The main measurement is the message behind the branching.

case Sends === to Each Branch

The file below sends the === message to a range object and looks at who answers it. Then it puts the same range in a when branch inside a case.

# range_match.rb — case sends the === message to each when branch
range = 1..10

puts range.class
puts range.method(:===).owner
puts range === 7

result = case 7
         when 1..10 then "inside"
         else "outside"
         end
puts result
Range
Range
true
inside

1..10 is a Range object, and Range answers the === message. When the range is asked about 7, the reply is true. When case 7 when 1..10 is written, the branch matches as well: case makes its value the argument of the === message sent to the object of the when branch. So when 1..10 is, behind the scenes, the question (1..10) === 7.

LB10. In this lesson, the “branch object” is the object written on the when line, and the “case value” is the object that comes after the case keyword. The receiver of the === message is the branch object, and its argument is the case value. The order matters: if 7 === (1..10) were written, the receiver would be 7 and the responder would change.

if and while Send No Message

We saw that the === message goes out in the case branches. The same question can be asked about if and while: is a condition chosen by a message? defined? answers this directly.

# condition_expressions.rb — if and while are syntax
puts defined?(if true then 1 end)
puts defined?(while false do end)
puts defined?(1 + 2)
puts defined?(case 1 when 1 then 2 end)
expression
expression
method
expression

if and while were reported as "expression"; that is, they are structures of the language, not method calls. 1 + 2, on the other hand, is "method". case also came out as "expression": the branching itself is syntax, but each when comparison inside it sends a message. There are two layers in the same structure: the case skeleton is syntax, and the choice of a branch is a message answered by the class of an object.

Statement Modifiers and the Value of if

In Ruby, a condition or a loop can be put at the end of a sentence. These are called statement modifiers: puts x if y, x += 1 until z. In addition, if produces a value; when no branch runs, that value is nil.

# statement_modifier.rb — statement modifiers and the value of if
x = 3
puts "big" if x > 2
x += 1 until x >= 6
puts x

y = if false then 1 end
p y
z = if true then 1 end
p z
big
6
nil
1

The first line ran because x > 2 was true. The until modifier repeated the addition until x reached six. if false then 1 end returned nil because it ran no branch, and if true then 1 end returned 1. The result: if is not a statement but an expression that produces a value. This is the same property as an assignment carrying the value 7.

Truthiness and the Value of a Loop

Whether a condition counts as true or false is also a rule of the language. In Ruby, only two values count as false: nil and false. Everything else, including 0 and the empty string, counts as true. The file below tries five values in the same condition.

# truthiness.rb — only nil and false count as false
[nil, false, 0, "", []].each do |value|
  status = if value then "truthy" else "falsy" end
  printf("%-5s %s\n", value.inspect, status)
end
nil   falsy
false falsy
0     truthy
""    truthy
[]    truthy

nil and false came out false, and 0, "", and [] came out true. This rule is the language’s own: only nil and false count as false, and every other value is true. Loops produce values too. If a while loop exits with break and a value, that value becomes the result of the loop; if it ends without break, the result is nil.

# loop_value.rb — while produces a value too
n = 0
result = while true
  n += 1
  break n * 10 if n == 3
end
p result

empty = while false do end
p empty
30
nil

The loop exited on the third round with 30, and result carried this value. The loop that never ran gave nil. This is the same behavior as if producing nil when no branch runs: in Ruby, control structures are expressions and have values.

Loops and the Values They Produce

The earlier sections measured branches; this section measures loops. The question is again about values: does a loop expression produce a value, and can break determine that value? The responder axis appears in two places in this section: while and until are keywords and syntax, while loop is a method and its owner is read from the output. The second block looks at a scope difference: does a variable of for behave the same way toward the outside as a block variable?

# loops.rb — the value of loops and next
result = loop { break 5 }
puts result
i = 0
i += 1 while i < 3
puts i
j = 10
j -= 1 until j < 8
puts j
empty = while false do end
puts empty.inspect
total = 0
(1..5).each do |n|
  next if n.even?
  total += n
end
puts total
puts method(:loop).owner
5
3
7
nil
9
Kernel

loop { break 5 } gave 5 as the value of the loop: the argument of break becomes the result of the loop expression. The modifier i += 1 while i < 3 raised i to 3, and j -= 1 until j < 8 stopped j at 7; while continues as long as the condition stays true, and until as long as the condition stays false. The expression while false do end produced a value although it never ran, and this value is nil. So a loop, like if, is an expression and carries a value. Inside (1..5).each, next if n.even? skipped the even numbers; the remaining 1, 3, and 5 were added up and 9 came out. next skips an iteration and break ends the loop; both are written inside a block and both are keywords of the language; this file printed no owner for them. The last line showed that the owner of loop is Kernel: loop is not a keyword but a method, like puts.

# for_leak.rb — the for variable leaks out, the block variable does not
[1, 2].each { |k| k }
d = defined?(k)
puts d.inspect
for m in [1, 2] do end
puts defined?(m)
puts m
puts [1, 2].method(:each).owner
puts 1.upto(3).to_a.inspect
puts 3.times.class
nil
local-variable
2
Array
[1, 2, 3]
Enumerator

When the variable k of the each block was asked about from outside, it returned nil: a block variable stays inside the block. for m in [1, 2] do end, on the other hand, leaked m to the outside: defined?(m) reported "local-variable", and in the end m was 2. So a for loop does not open a new scope. The scope behavior of two spellings that do the same job differs; this difference turns the choice between them from a matter of taste into a matter of consequence. [1, 2].method(:each).owner gave Array; that is, the class of the array answers each. 1.upto(3).to_a gave [1, 2, 3], and 3.times, called without a block, returned an Enumerator: times called without a block gives an iterator instead of producing a value.

LB11. That for calls the each message of the array behind the scenes was not measured. What was measured is only the scope difference and that the owner of each is Array.

Short-Circuiting and Comparison

Conditions are not built only with if and case: the || and && operators also decide whether to run the right side. This section measures two questions: what is observed when an operator skips evaluating the right side, and who owns the methods that do comparison? The axis is again the responder; || and && are syntax of the language, while <=> and between? are messages.

# short_circuit.rb — does the right side run, and who answers
def visit(name)
  puts name
  true
end
visit("a") || visit("b")
visit("c") && visit("d")
v = nil || "fallback"
puts v
w = false && 1
puts w.inspect
puts 1 <=> 2
puts 5.between?(1, 10)
n = 5
verdict = n > 3 ? "big" : "small"
puts verdict
puts 5.method(:<=>).owner
puts 5.method(:between?).owner
a
c
d
fallback
false
-1
true
big
Integer
Comparable

On the line visit("a") || visit("b"), only a was printed: because the left side was true, the right side never ran. On the line visit("c") && visit("d"), both c and d were printed, because && also looks at the right side when the left side is true. This is short-circuiting: if the result is already determined by the left side, the right side is not evaluated. The expression nil || "fallback" gave fallback; || does not only produce a truth value, it returns the operand itself. false && 1 returned false. Giving a default value with || relies on this behavior: if the left side is nil or false, the value on the right returns.

1 <=> 2 gave -1: the comparison message reports the ordering as an integer. 5.between?(1, 10) was true. The ternary operator n > 3 ? "big" : "small" gave big. The owner lines revealed two different responders: Integer answers the <=> message, while the Comparable module answers the between? message. Both are sent to a number and give a result, but the places where they are defined differ. So the comparison messages used when writing conditions are divided between the methods of the class itself and the methods of a shared module.

Error Flow: rescue, else, and ensure

Control flow does not consist of conditions and loops alone; an error changes the flow as well. This section measures the order in which the rescue, else, and ensure branches run, and which of them determines the value of the method. The error class is printed, its message is not. The axis is syntax: rescue and ensure are keywords, while raise is a method.

# exception_flow.rb — the order of rescue, else, and ensure
def guarded
  yield
rescue ZeroDivisionError => e
  puts e.class
  :rescued
else
  :clean
ensure
  puts "cleanup"
end
a = guarded { 1 / 0 }
puts a.inspect
b = guarded { 1 / 1 }
puts b.inspect
c = begin
  Integer("x")
rescue ArgumentError
  -1
end
puts c
puts method(:raise).owner
puts method(:guarded).owner
ZeroDivisionError
cleanup
:rescued
cleanup
:clean
-1
Kernel
Object

In the first call, the block computed 1 / 0 and raised ZeroDivisionError. The rescue branch ran, printed the class, and returned :rescued; then the ensure branch wrote cleanup. The return value stayed :rescued: ensure does not determine the value of the method. In the second call no error occurred, so the else branch ran and the value of the method became :clean; ensure again wrote cleanup. So ensure runs on both paths, rescue only when there is an error, and else only when there is none.

A begin block is also an expression and carries a value. Integer("x") raised ArgumentError, the rescue branch returned -1, and c took this value. This is the counterpart, in error flow, of if producing a value: one of the branches runs, and the last expression of the branch that ran becomes the value of the whole. The owner lines showed Kernel for raise and Object for guarded: raising an error is a method call, but rescue and ensure are the language’s own spelling. The text of the error message was not printed in this section, because the text of a message may change between versions, while its class does not.

The Bounding Measurement

The responder of this lesson is a class. The slip here is to take the responder of case to be the class of the value given to case. Yet in the expression case 7, the class of 7 is Integer, but the receiver of the === message is the branch object. For this reason, for the same 7, Module answers on the line when Integer, Regexp on the line when /a/, and Range on the line when 1..10.

# branch_object.rb — the responder of case is the class of the branch object
branches = [Integer, /a/, 1..10]

branches.each do |branch|
  printf("%-8s class %-8s owner %s\n", branch.inspect, branch.class, branch.method(:===).owner)
end

puts Integer === 7
puts(/a/ === "table")
puts branches[2] === 7
Integer  class Class    owner Module
/a/      class Regexp   owner Regexp
1..10    class Range    owner Range
true
true
true

Three branch objects, three different owners: for Integer, the Module class answers the === message (the Integer.=== observation from the previous lesson), for the regular expression Regexp does, and for the range Range does. Even when the value of case is always 7 or the same "table", the only thing that changes is the receiver. What is read as the responder class is the class of the branch object, not of the case value.

The two layers measured in this lesson must be read together. The skeleton of a control structure, that is, the words if, while, and case, is the language’s own spelling, and defined? reports them as "expression". The comparison inside a branch, on the other hand, is a message, and the class of an object answers it. Separating the two turns the question “why did this branch match” into the question “what does the === method of the branch object do” when reading a case expression.

Summary

  • case sends the === message to the branch object in each when branch; when 1..10 is, behind the scenes, the question (1..10) === 7.
  • The receiver of the === message is the branch object and its argument is the case value; if the order is reversed, the responder changes.
  • if and while are reported as "expression" by defined?; they are structures of the language and send no message.
  • Statement modifiers (if, unless, while, until) make a sentence conditional or looped; if produces a value, and returns nil when no branch runs.
  • The responder of this lesson is a class: the responder of case is the class of the branch object, not of the case value.
  • loop { break 5 } returns 5 and while false do end gives nil; the for variable leaks out and the block variable does not; || and && do not run the right side unless needed, and ensure runs on every path.

Next Step

We measured who answers the condition and the choice of branch. But when the receiver is never written in a method call, for example when puts or format is written, who handles the message? The next lesson, Methods, measures the responder of a receiverless call.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close