Skip to content

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

  1. 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 ||= compute where compute returns false; call it three times and count how often compute ran, and fix it with a defined?(@fixed) guard.
  2. Write load_config(path) as File.read(path) or raise "no config" and explain why the raise never fires; rewrite it so that it does, once with || and parentheses and once with rescue, 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.

a = nil || "x"      -> a is "x"
b = nil or "x"      -> b is nil
c = (nil or "x")    -> c is "x"
1 and 2              -> 2
nil or 1 and 2       -> 2
!true == false       -> true
not true == false    -> true
@trap ||= compute, called 3 times  -> compute ran 3 time(s)
defined?(@fixed) guard, called 3 times -> compute ran 1 time(s)

See also