and/or bind lower than =¶
Level: 201 · anyone who has written x = a or b and been handed a
One line: Ruby has two pairs of boolean operators — &&/|| bind tightly and are the ones for expressions, while and/or bind lower than = and equally with each other, so x = false or true assigns false; Python has one pair, and/or, binding above =, with and tighter than or.
|| and && sit high in the precedence table, above =, so x = false || true assigns the whole disjunction. and and or — Perl's spellings (Not machine-checked here.) — sit at the bottom, below = and below not: x = false or true parses as (x = false) or true, and the assignment quietly stores false while the expression as a whole is true. They are meant for control flow between whole statements — save or raise "boom" — not for computing values. Both pairs return an operand rather than a boolean, which is why nil || "default" works as a default. ! is a method (so a class can define it) and binds to the nearest term; not applies to the whole comparison after it. Comparisons do not chain, because 1 < x is already true and true has no <. The program measures each of these.
Verified output of and_or_precedence_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
1. x = false || true; x -> true
2. x = false or true; x -> false (the whole expression is true)
3. y = nil or "d"; y -> nil
4. nil || "d", 1 && 2, nil && 2 -> "d", 2, nil
5. true or false and false -> false ((true or false) and false)
6. !nil == false / not nil == false -> false / true
7. !obj is a method; && and || are not -> !Loud.new -> "custom !"; Loud.new && 1 -> 1; 1.respond_to?(:"&&") -> false
8. a ||= 5 for nil / false / 0 / "" -> 5 / 5 / 0 / ""
9. 1 < x < 3 with x = 2 -> NoMethodError: undefined method '<' for true
10. p(true or false) / p((true or false)) -> SyntaxError / compiles
Reading the output¶
Rows 1 to 3 are the trap: x = false || true leaves x as true, x = false or true leaves it as false — the or took the assignment as its left operand, and only the expression as a whole is true — and y = nil or "d" leaves y as nil, the default never assigned. Row 4: || and && return whichever operand decided the answer — "d", 2, nil — not true/false. Row 5: and and or share one precedence level and group left to right, so true or false and false is (true or false) and false, which is false. Row 6: !nil == false is (!nil) == false, true == false, false; not nil == false is not (nil == false), true.
Row 7: ! is an ordinary method — Loud defines it to return a string, and !Loud.new returns that string — while && and || are syntax: 1.respond_to?(:"&&") is false, and Loud.new && 1 is 1 because any object that is not nil or false is truthy, with no hook to change that. Row 8: a ||= 5 assigns only when a is nil or false, so 0 and "" stay as they are — they are truthy in Ruby. Row 9: 1 < x < 3 raises NoMethodError: 1 < 2 is true, and true has no <. Row 10: and/or cannot sit bare inside an argument list — p(true or false) is a SyntaxError — and a second pair of parentheses, which makes it a grouped expression, is the fix.
Compared with Python¶
Python has only and, or and not; ||, &&, ! and ||= are syntax errors (rows 1, 6 and 8). Its or binds above =, so x = False or True assigns True (rows 1 and 2) and y = None or "d" assigns 'd' (row 3) — the Ruby trap has no Python counterpart. Both return an operand (row 4). Precedence between the two differs: and binds tighter than or, so True or False and False is True (row 5) where Ruby's equal-precedence pair gave false. Row 7 shows that Python's truthiness is a protocol where Ruby's is fixed: not and and both called Loud.__bool__. Row 8 is the trap Python has instead of Ruby's: a = a or 5 replaces 0 and "" because they are falsy, so a default written that way overwrites a legitimate zero. Row 9: comparisons chain, 1 < x < 3 means 1 < x and x < 3, and the twin counts that x was evaluated once. Row 10: and/or are ordinary expressions and can be an argument.
Verified output of and_or_precedence_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. x = false || true; x -> SyntaxError for ||, SyntaxError for &&; x = False or True gives True
2. x = false or true; x -> True (the whole expression is True)
3. y = nil or "d"; y -> 'd'
4. nil || "d", 1 && 2, nil && 2 -> 'd', 2, None
5. true or false and false -> True (True or (False and False))
6. !nil == false / not nil == false -> SyntaxError for !None / True
7. !obj is a method; && and || are not -> not Loud() -> False; Loud() and 1 -> 1; both via ['__bool__', '__bool__']
8. a ||= 5 for nil / false / 0 / "" -> SyntaxError for ||=; a = a or 5 gives 5 / 5 / 5 / 5
9. 1 < x < 3 with x = 2 -> True; 1 < 2 < 0 -> False; x evaluated 1 time(s)
10. p(true or false) / p((true or false)) -> compiles / compiles
The Python library has no page on operator precedence; the reference's operator precedence table ↗ is the source, beside Ruby's precedence page ↗.
Try it¶
- Predict, then print:
a = nil || "x",b = nil or "x",c = (nil or "x"),1 and 2,nil or 1 and 2,!true == false,not true == false. Then write a class whose method memoizes with@trap ||= computewherecomputereturnsfalse; call it three times and count how oftencomputeran, and fix it with adefined?(@fixed)guard. - Write
load_config(path)asFile.read(path) or raise "no config"and explain why theraisenever fires; rewrite it so that it does, once with||and parentheses and once withrescue, and decide which reads better.
Solution to 1
and_or_precedence_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.
# Exercise 1: predict, then print, a handful of precedence puzzles; then
# show the ||= memoization trap on a method whose answer is false.
a = nil || "x"
b = nil or "x"
c = (nil or "x")
puts "a = nil || \"x\" -> a is #{a.inspect}"
puts "b = nil or \"x\" -> b is #{b.inspect}"
puts "c = (nil or \"x\") -> c is #{c.inspect}"
puts "1 and 2 -> #{(1 and 2).inspect}"
puts "nil or 1 and 2 -> #{(nil or 1 and 2).inspect}"
puts "!true == false -> #{(!true == false).inspect}"
puts "not true == false -> #{(not true == false).inspect}"
class Report
def initialize
@computed = 0
end
def trap?
@trap ||= compute
end
def fixed?
return @fixed if defined?(@fixed)
@fixed = compute
end
def computed = @computed
private
def compute
@computed += 1
false
end
end
r = Report.new
3.times { r.trap? }
puts "@trap ||= compute, called 3 times -> compute ran #{r.computed} time(s)"
r = Report.new
3.times { r.fixed? }
puts "defined?(@fixed) guard, called 3 times -> compute ran #{r.computed} time(s)"
Verified output of and_or_precedence_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
See also¶
- Everything is an expression, even
defandclass— an assignment is an expression, which is whyorcan take one as its left operand unlessnegatesif; modifiers hang off the end of a line — the other Perl-flavoured spellings, at the same low precedence asand/ornil,falseand truthiness — why0and""survive||=in Ruby and notorin Python||=and nil guards — the memoization trap of row 8 and the kata, in full- Operators are methods —
!is a method and&&/||/and/orare not, from the other side - Gotchas for Python programmers —
x = a or band1 < x < 3in the hub of traps - Ruby docs: precedence ↗ — the full table,
!at the top andand/orat the bottom - Twenty-three Python traps for a Ruby programmer — The twenty-three places where a Ruby reflex gives the wrong answer in Python