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¶
- Drain
queue = [3, 1, 2]three ways: withuntil queue.empty?, withputs … while queue.any?as a modifier, and with abegin … 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. - Translate Python's
for … elsesearch into Ruby three ways:find,eachwithbreakand a flag variable, andcatch/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.
See also¶
- Everything is an expression, even
defandclass— whyx = unless … endandv = loop do … endare assignments that work and/orbind lower than=— the other Perl-flavoured spelling, and the modifier-styledo_it or raiseidiomforloops do not make a scope — what afor, awhileand a modifier loop leave behindrescueas a modifier — the fifth modifier, and where its precedence bitesnext,breakandreturnin blocks —break vas the value ofloopand ofeach- Gotchas for Python programmers —
elsif,end, andx = 5 if falseleavingxasnil, in the hub of traps - Rust: the
loopkeyword ↗ — Rust'sloopis Ruby'sloop do, down tobreakcarrying a value - Rust:
whileloops ↗ — another language without a do-while - Ruby docs: control expressions ↗ —
unless,until, the modifiers andbegin … end while, from the reference - Twenty-three Python traps for a Ruby programmer — The twenty-three places where a Ruby reflex gives the wrong answer in Python
- There is more than one way, and one obvious way — Ruby's idioms