Sunday, August 05, 2007

Ruby: operator precedence of && and = (which also applies to || or)

Recently a bug popped up on my project surrounding code similar to the following sample.

total = shopping_cart.empty? and shopping_cart.total

The problem with the above code is that it evaluates as if it were the snippet below.

(total = shopping_cart.empty?) and shopping_cart.total

It should be obvious that setting the total to true or false is not the intended behavior.

The problem with the original snippet is the usage of the 'and' operator. The Ruby operator precedence table shows that '=' has as higher precedence than the 'and' operator.

The solution is to use the '&&' operator instead of the 'and' operator. The precedence table shows that the '&&' operator has a higher precedence than the '=' operator. Therefore, the following two lines of code are basically the same.

total = shopping_cart.empty? && shopping_cart.total
total = (shopping_cart.empty? && shopping_cart.total)

As the title says, the same rules apply to the precedence of '||' and 'or'.

8 comments:

  1. Though I prefer 'and' and 'or' for readability, I no longer use either, ever. Using && and || exclusively obviates subtle bugs popping up due to the precedence issues you mention here. Those kinds of bugs aren't worth the pinch of readability sugar the word forms provide.

    ReplyDelete
  2. nIf you look back at the history of Perl, which is where 'and' and 'or' come from, they were explicitly introduced not for readability, but because sometimes the low precedence operators were what you wanted.

    In Perl, with it's assignment contexts, it really makes sense. Code like:

    my @fileinfo = stat($file) or die;

    behaves very differently from:

    my @fileinfo = stat($file) || die

    In the second case, the stat gets forced into a scalar context, which is almost certainly not what you want.

    About the only place you see the 'english' logical operators in good perl is in the 'assignment or die' idiom. In Ruby it makes sense to use them in that context too:

      value = possibly_false || raise "foo"

    is a syntax error, and so is:

      (value = possibly_false) || raise "foo"

    but

      value = possibly_false or raise "foo"

    works fine.

    Me, I like the '... or raise' approach, but that's pretty much the only place I'd ever use 'or'. As for 'and', it's almost completely useless, but once you've got 'or'...

    ReplyDelete
  3. why write something so tricky?

    total = shopping_cart.total unless shopping_cart.empty?

    ReplyDelete
  4. Agree with jchris. It's more readable, and faster to boot (from a couple of quick benchmarks I just decided to do anyways).

    ReplyDelete
  5. "why write something so tricky?"

    Because the version you posted isn't an example that demonstrates how behavior changes based precedence.

    If I were trying to solve the problem, I too would prefer the snippet you posted, but that wasn't the point.

    ReplyDelete
  6. The set of rules to avoid these problems is very simple, imo:

    Use "and" and "or" when asignments are not involved.
    ie:
    if thinga and thing b
    ...
    end

    Use "&&" and "||" when asignment is involved
    ie: result= a || b

    Never mix them!!!
    ie: result= a || b and c
    (note you can use them to avoid the need to parentesis here, but maybe is to much to take into account and error prone)

    ReplyDelete
  7. The append operator is the same way!
    I recently came across a problem doing:
    x = []
    x << y ? "something" : ""

    DOES NOT WORK because the precidence of << is higher than ?: operator. How can this be? When is that useful?

    -David

    ReplyDelete
  8. Wait a minute. The problem is totally with your example. Why do you think that "it should be obvious that setting the total to true or false is not the intended behavior"???

    total = shopping_cart.empty? and shopping_cart.total

    As a sentence this says to me, set total to the value of the emptiness of the shopping cart and the the total of the shopping cart combined (true && 100, or false && 100). Not knowing anything about Ruby, I would expect a true or a false back.

    Of course, Ruby is more generous than that; it will actually give you back the total.

    >> shopping_cart_empty = true
    => true
    >> shopping_cart_total = 100
    => 100
    >> total = shopping_cart_empty and shopping_cart_total
    => 100
    >> total = (shopping_cart_empty and shopping_cart_total)
    => 100
    => true

    Most programmers would write that sentence the other way, though, since the shopping_cart.total seems to be what's important and the emptiness of the shopping cart is a limiting condition:

    total = shopping_cart.total and shopping_cart.empty?

    >> total = shopping_cart_total and shopping_cart_empty
    => true
    >> total = shopping_cart_total && shopping_cart_empty
    => true
    >> total = (shopping_cart_total && shopping_cart_empty)
    => true
    >> shopping_cart_empty = false
    => false
    >> total = (shopping_cart_total && shopping_cart_empty)
    => false

    This shows that the first term is checked and the second value is given if it is true, false if not. Actually, the not-true value is given:

    >> shopping_cart_empty = nil
    => nil
    >> total = shopping_cart_total and shopping_cart_empty
    => nil
    >> total = shopping_cart_empty and shopping_cart_total
    => nil

    In the other order, 100 was returned because it had a not-nil value and was the second term.

    As others have said, it makes much more sense to just say:

    total = shopping_cart_total if shopping_cart_empty

    'and' doesn't make a lot of sense if you're interested in a value and not a boolean comparison. Getting out a non-boolean value is just a hack. We should look at why Ruby programmers think you can do this ever:

    >> bar = nil
    => nil
    >> baz = "hello"
    => "hello"
    >> foo = bar or baz
    => "hello"
    >> foo = baz or bar
    => "hello"
    >> foo = (baz or bar)
    => "hello"
    >> baz = nil
    => nil
    >> foo = (baz or bar)
    => nil

    The 'or' behavior is more intuitive, because you can get either the left or the right one back, depending on whether the first term is nil or not. In the case where both baz and bar are nil, you're technically getting bar back:

    >> baz = nil
    => nil
    >> bar = false
    => false
    >> foo = (baz || bar)
    => false
    >> foo = (bar || baz)
    => nil
    >> foo = bar or baz
    => nil

    So it's the same as the and behavior. In an and, when two things together are 'and', you get the right term back. In an or, when two things together are neither of them true, you get the right not-true value. I don't see that assignment is the problem.

    Please tell me if I'm full of it.

    ReplyDelete

Note: Only a member of this blog may post a comment.