Operator Precedence in Ruby

When writing Ruby code, you might occasionally come across some odd behavior caused by different precedence of seemingly identical operators. The best-known cases are probably the and and or operators and their counterparts, && and ||. The spelled-out operators have lower precedence than their symbolic variants. In particular, the precedence of and and or is lower than that of the = assignment operator.

A common use of the || operator is to assign a default value:

variable = value || default

Due to its lower precedence, this line behaves differently when replacing || with or. The line with or gets evaluated as:

(variable = value) or default

This is of course not what we would expect in this situation, and errors caused by this subtle difference tend to be pretty hard to track down. However, we can make use of this behavior in another common use case.

if (variable = object.value) && boolean?
	[...]
end

When using and we can omit the brackets and rely on the lower precedence of this operator:

if variable = object.value and boolean?
	[…]
end

There are other differences in precedence that are less well-known. For example, take a look at the following piece of RSpec test code:

expect{ test }.to raise_error do |e|
  e.should be_a(NewException)
  e.message.should eq("message")
end

The code looks alright, and the worst part: it passes. In fact, it always passes, provided that our test method raises an exception. This is due to the lower precedence of the do-end block syntax compared to { }. However, this code will be evaluated as:

expect{ test }.to(raise_error) do |e|
  e.should be_a(NewException)
  e.message.should eq("message")
end

So, the block is passed to RSpec's to method that ignores it. If we enclose the block in curly braces, the code is evaluated as expected:

expect{ test }.to raise_error { |e|
  e.should be_a(NewException)
  e.message.should eq("message")
}

This is evaluated as:

expect{ test }.to(raise_error { |e|
  e.should be_a(NewException)
  e.message.should eq("message")
})

The block is passed to the raise_error method, which brings us the desired behavior.

Differences like these can be very confusing for beginners and the forgetful alike. Therefore, we try to avoid the spelled-out logical operators, and rather make the expected behavior explicit by adding a pair of redundant brackets.

Unfortunately, the difference in precedence of the block boundary operators cannot be avoided as easily. Therefore, it is advisable to internalize them and leave a note for future readers of your source code. To indicate that the curly braces are actually required, we also add a pair of implicit round brackets to the test code.

Also, these subtle differences make a compelling argument for test-driven development. Writing the test prior to implementing the behavior to be tested lets you recognize that the behavior isn't tested the way it should.