Skip to content

unless negates if; modifiers hang off the end of a line

Level: 101 · a Python programmer meeting unless, until and x if c for the first time

One line: Ruby spells if not as unless and while not as until, lets any statement carry if, unless, while or until at its end, and gives a do-while with begin … end while; Python has none of those spellings and no do-while, but has the for … else that Ruby lacks.

unless c runs its body when c is false and may take an else, but not an elsif. until c loops while c is false. Both, and if and while, can follow a statement as a modifier — retry if attempts < 3, queue.shift until queue.empty? — and a modifier tests its condition before the first run, exactly like the block form. The one exception is begin … end while c: a begin block with a while or until modifier runs its body once before testing, which is Ruby's do-while. loop do … end runs until break, and break's argument becomes the loop's value. The spelling of else-if is elsif, and the program measures what happens when a Python programmer types elif or else if instead: a syntax claim is measured by compiling a string and printing the exception class, so nothing here is asserted from memory.

Verified output of unless_until_and_modifiers_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

 1. unless 1 > 2 ... else ... end            -> "body"
 2. unless ... elsif ... end                 -> SyntaxError
 3. n = 0; until n >= 4 ... n += 1           -> 4
 4. r << x if c; r << y unless c             -> ["if", "unless"]
 5. n = 10; n += 1 while n < 3               -> 10  (tested first, never ran)
 6. n = 10; begin n += 1 end while n < 3     -> 11  (ran once, then tested)
 7. loop do ... break i * 10 ... end         -> 30
 8. if/elsif/else/end                        -> compiles
 9. if ... elif true ... end                 -> compiles; when that branch runs: NoMethodError
10. else if ... with one end / two ends      -> SyntaxError / compiles
11. y = 5 if false; then y                   -> nil  (defined? -> "local-variable")
12. for ... else (search with no hit)        -> SyntaxError; find -> 9 / nil

Reading the output

Row 1: with 1 > 2 false, unless ran its body, so x is "body". Row 2: unless … elsif does not compile — Ruby's grammar gives unless an else and nothing more, since "unless this, else if that" reads badly. Row 3: until n >= 4 counted n up to 4. Row 4: the trailing forms r << "if" if true and r << "unless" unless false both ran.

Rows 5 and 6 are the pair to remember. n += 1 while n < 3 with n at 10 leaves n at 10: a modifier while tests first and the body never ran. begin n += 1 end while n < 3 leaves n at 11: with a begin … end block in front, the body runs once before the test. That is the do-while, and it is the only case in which a modifier does not test first.

Row 7: loop do … end is a method with a block, not a keyword; break i * 10 ended it and made 30 the value of the whole loop call. Row 8: elsif compiles. Row 9: elif true also compiles — not because Ruby knows elif, but because it parses as a call to a method named elif with the argument true; the mistake surfaces only when that branch runs, as a NoMethodError. Row 10: else if opens a second, nested if that needs its own end, so with one end it is a SyntaxError and with two it compiles — as two nested conditionals, not as an else-if. Row 11: y = 5 if false never assigns, but the parser has seen y = …, so y exists and is nil — defined?(y) says "local-variable". Row 12: for … else does not compile; the search-with-a-miss idiom is find, which returns the hit or nil.

Compared with Python

Python negates with not and has no second spelling: unless and until are syntax errors (rows 1 and 3), and if not c: keeps its elif where Ruby's unless refuses elsif (row 2). Nothing can trail a statement (rows 4 and 5): Python's x if c else y is an expression, must have its else, and cannot stand in for a statement modifier. There is no do-while (row 6) — the idiom is while True: with a break at the bottom — and no loop keyword (row 7); while True: serves both. elif is the spelling (rows 8 and 9), and else if is an error while else: with a nested if compiles (row 10), just as in Ruby. Row 11 is the sharpest contrast: after if False: y = 5, the name y does not exist and reading it raises NameError, where Ruby's parser had already created the variable. Row 12 is the feature Python has and Ruby lacks: for … else, whose else runs only when the loop finished without break — so the twin prints break at 9 for one list and else ran for the other.

Verified output of unless_until_and_modifiers_py.py — regenerated by tools/run_examples.py, never hand-typed.

 1. unless 1 > 2 ... else ... end            -> SyntaxError  (if not 1 > 2: gives 'body')
 2. unless ... elsif ... end                 -> compiles  (if not c: ... elif d: ...)
 3. n = 0; until n >= 4 ... n += 1           -> SyntaxError  (while not n >= 4: gives 4)
 4. r << x if c; r << y unless c             -> SyntaxError  (an expression needs else: x if c else y)
 5. n = 10; n += 1 while n < 3               -> SyntaxError  (while n < 3: n += 1 gives 10)
 6. n = 10; begin n += 1 end while n < 3     -> SyntaxError  (while True: ... if not c: break gives 11)
 7. loop do ... break i * 10 ... end         -> SyntaxError  (while True: ... v = i * 10; break gives 30)
 8. if/elsif/else/end                        -> SyntaxError
 9. if ... elif true ... end                 -> compiles
10. else if ... with one end / two ends      -> SyntaxError / compiles  (else if / else: nested if)
11. y = 5 if false; then y                   -> SyntaxError  (if False: y = 5; then y -> NameError)
12. for ... else (search with no hit)        -> compiles; break at 9 / else ran

The Python library has no page on these statements; the reference for if, while and for … else ↗ is the language manual.

Try it

  1. Drain queue = [3, 1, 2] three ways: with until queue.empty?, with puts … while queue.any? as a modifier, and with a begin … end until queue.empty? body that runs once even when the queue starts empty. Print each element as it leaves and, for the last form, how many times the body ran.
  2. Translate Python's for … else search into Ruby three ways: find, each with break and a flag variable, and catch/throw — then decide which one you would keep.
Solution to 1

unless_until_and_modifiers_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.

# Exercise 1: drain a queue three ways -- until, a while modifier, and a
# begin ... end until body that runs once even when the queue starts empty.

queue = [3, 1, 2]
until queue.empty?
  puts "until loop took #{queue.shift}"
end

queue = [3, 1, 2]
puts "modifier took #{queue.shift}" while queue.any?

queue = []
tries = 0
begin
  tries += 1
  puts "begin...end until: pass #{tries}, queue empty? #{queue.empty?}"
end until queue.empty?
puts "the body ran #{tries} time(s) although the queue was empty from the start"

Verified output of unless_until_and_modifiers_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

until loop took 3
until loop took 1
until loop took 2
modifier took 3
modifier took 1
modifier took 2
begin...end until: pass 1, queue empty? true
the body ran 1 time(s) although the queue was empty from the start

See also