30 September 2013

A Sliver of Ruby #2: Ruby's Logical Operators (and their proper use)

Ruby has two triads of logical operators. For those new to Ruby, this can be a source of confusion: in some common cases, the corresponding operators from each triad are interchangeable; in other cases, their behavior is quite different.

The first set of the triad is && , || and ! — the "logical and, logical or, logical not."  This group is best used within an expression, to combine a bunch of conditions together.

The second is and , or and not— the "logical composition operators".  These operators are intended to be used between statements to conditionally combine them.

The Same on the Surface But Different Underneath

Both set of operators calculate expressions the same way.

That is: both && and and take two operands and logically and them together; both || and or take two operands and logically or them together.  Likewise, ! and not both negate the expression.

apple = true
orange = false

apple && orange  # => false
apple and orange # => false
apple || orange  # => true
apple or orange  # => true

!apple           # => false
not apple        # => false
!orange          # => true
not orange       # => true

This is where the similarities end. There are two fundamental differences.

Difference #1: Short-Circuiting

&& and || can short-circuit the remainder of the expression; whereas and and or only short-circuit between the two operands.

# Only if the souschef fails to wash, will the chef wash.
# but in either case, the chef always slices the apple.
souschef.wash(apple) or chef.wash(apple) and chef.slice(apple)

# if the souschef does peel the orange, nothing more happens.
# else, the chef peels the orange (and if successful) slices it.
souschef.peel(orange) || chef.peel(orange) && chef.slice(orange)

Difference #2: Precedence

and, or and not are among the lowest precedence operators in the Ruby language.  In contrast, &&, || and ! are relatively higher.  This is particularly important when your expression includes other operators that sit between these two sets in the precedence ranking.

Here's the complete set of operators in Ruby, in order of precedence.

Symbol
Name
::
Constant resolution operator
[] []=
Element reference operator and element set operator 
**
Exponent operator (raise to the power of...) 
! ~ + -
Unary operators: not, complement, unary plus, unary minus 
* / %
Multiplicative operators: multiply, divide, modulo 
+ -
Addition and subtraction operators 
<< >>
Bitwise shift operators (left and right) 
&
Bitwise "and" operator
| ^
Bitwise "or" and bitwise "xor" operators
> < >= <=
Comparison operators 
== === <=> =~
Equals, same object, "spaceship"/compare, and regex match operators
&&
Logical "and" operator
||
Logical "or" operator
.. ...
Range operators: inclusive and exclusive
? :
Ternary operator 
=
Assignment operator
defined?
Symbol existence operator 
not
Logical negation operator
or and
Logical composition operators
if unless while until
Flow control operators 
begin/end
Block expression 
Table 1 —  Ruby operators in order of precedence.  Highlighted sit between the two triads.

For example:

Using the Logical "and" operator, this statement is non-sensical...

# The chef washes (nil), and food becomes
# whether she is successful AND whether orange is truthy
food = orange && chef.wash(food)

whereas, with the logical composition "and", we have a series of statements strung together:

# The food is orange
# And the chef washes the food (i.e. the orange)

food = orange and chef.wash(food)

That's a lot of detail to keep in mind. The point is to appreciate that even though the two operator sets appear to behave the same superficially, deep down, they are different and not always interchangeable.

Conclusion

Where you go from here depends on the skill level of the "average committing developer" for your product, both present and future (see Dryfus Model of Skill Acquisition for definitions).

Novice and Advanced Beginner

In this case, you're likely working in a shop that leans more toward being productive over engineering a solution.

The most professional move here is to keep it as simple as possible.  In this way, the system has a better chance of being maintainable.

Avoid and prohibit use of the logical composition operators (and, or, and not) all together.  There is no significant loss of expressiveness (i.e. there are no significant statements that you suddenly can't write) and it avoids this confusion completely.

Competent 

There is room/expectation for some deliberation, but you're mostly writing application code and not so many frameworks and/or libraries.  That is: your code is rather concrete and therefore more straightforward.

In these circumstances, you can certainly make use of the additional expressiveness that the logical composition operators bring.  However, you'll likely want to define the context for when programmers ought to use each.  Include include these in your coding style guide.

Here's a pair as an example...

Guideline #1: Use &&, || and ! for boolean expressions

Focus/limit your use of the symbolic set of logical operators (&&, || and !) for composing boolean expressions.

For example:
if params[:order] && params[:order][:coupon_code]

not

if params[:order] and params[:order][:coupon_code]
... and ...

return false if !Rails.env.production? ||
                !Spree::Config[:check_for_spree_alerts]
instead of
return false if !Rails.env.production? or
                !Spree::Config[:check_for_spree_alerts]


Guideline #2: Use and, or, and not for flow control

Use the word set (and, or and not) for conditionally stringing statements together. In other words, for flow control.

Examples:

respond_with(@order) { render :edit } and return
not
respond_with(@order) { render :edit } && return

... and ...

get_from_cache(id) or load_from_disk(id) and send_email()
instead of
(get_from_cache(id) || load_from_disk(id)) && send_email()

Proficient and Expert

You're in a the-product-is-the-software company and building not just the end products, but an ecosystem of compose-able components, flexible frameworks and multi-purpose libraries.

Chances are, you're not reading this article. :)  But for completeness' sake, the advice here is to use the full power of the toolset you have.

Avoid unnecessary parenthesis and loss of expressiveness by using the right operator for the job.

Example: Pre-Checks


(from https://github.com/spree/spree/blob/master/core/lib/spree/responder.rb):

def to_html
  super and return if !(on_success || on_failure)
  has_errors?
    ? controller.instance_exec(&on_failure)
    : controller.instance_exec(&on_success)
end

Here, there's an early return at the top of the method if there are no hooks registered.  Pre-checks like this are reasonably easy to learn idioms and help avoid verbose guards (e.g. an if statement surrounding the contents of the method).

Notice, BDQ's thoughtful use of both kinds of logic operators that visually convey the difference uses (i.e. he used and specifically for logical composition and || for boolean math).

Example: Precedence

(from https://github.com/rails/rails/blob/master/activerecord/lib/active_record/associations/has_many_association.rb)

def count_records
  ...
  @target ||= [] and loaded! if count == 0
  ...
end

Here, Leighton is making use of the fact that assignment has a higher precedence than the logical composition operator.  This is a little more straight-forward and esthetically pleasing than:

def count_records
  ...
  (@target ||= []) && loaded!() if count == 0
  ...
end

(I added the second set of parens after "loaded!" to call out the fact that they would be needed had the method required parameters.)

References

No comments:

Post a Comment