hardscrabble 🍫

Psst. Check out my RubyConf 2017 talk, There are no rules in Ruby.

order of operations

16 Jun 2015

Last month, we looked at Ruby operators, and I complained about how I wish I could define my own operators. Today I’m looking at them a little more, and thinking about how Ruby handles expressions made up of multiple operations.

Let’s say you have this Ruby code:

sum = a + b + c

What are a, b, and c? They could be

  1. variables containing numbers; that’s what they kind of look like they want to be
  2. invocations of methods which return numbers
  3. variables or methods that contain/return absolutely anything else; let’s not worry about this
  4. maybe they’re not even defined at all; let’s not worry about this possibility either

Let’s look at how option 1 plays out:

a = 1
b = 1
c = 1
puts a + b + c
# 3

So far, so good.

Let’s see how option 2 plays out:

def a; 1; end
def b; 1; end
def c; 1; end
puts a + b + c
# 3

Sort of funky-looking, but also sort of straight-forward. Here’s the question though: if Ruby is calling those 3 methods to get those 3 values, what order are they being called in? Let’s find out:

def a
  puts "a"
  1
end

def b
  puts "b"
  1
end

def c
  puts "c"
  1
end

puts a + b + c
# a
# b
# c
# 3

It kind of makes sense. It’s just going from left to right, like English.

One cool thing about Ruby is that (almost) everything is an object, and even core things like math operations are implemented as methods. This means the above could be written like this:

puts a.+(b).+(c)
# a
# b
# c
# 3

This rendition makes it clear that this is a chained sequence of method calls. Let’s make it even more clear, by refining the plus method and adding some logging:

module MathLogger
  refine Fixnum do
    alias_method :original_plus, :+

    def +(other)
      original_plus(other).tap do |sum|
        puts "#{self} + #{other} = #{sum}"
      end
    end
  end
end

using MathLogger

def a
  puts "a"
  1
end

def b
  puts "b"
  1
end

def c
  puts "c"
  1
end

puts a.+(b).+(c)
# a
# b
# 1 + 1 = 2
# c
# 2 + 1 = 3
# 3

Now it’s not as simple as “left to right”. We start at the left and call the a method. But the next method we call is b, not +. Before we can add two values, we need to know what the values are, and Ruby will evaluate the expression in parentheses (here it’s calling a method, but it could be calling multiple methods and they would all be evaluated before the + method is called).


A brief digression about defined?

This rule doesn’t apply to the defined? method, which ships with Ruby and behaves like this:

msg = "Hello"
defined?(msg) #=> "local-variable"
OMG           #=> NameError: uninitialized constant OMG
defined?(OMG) #=> nil
OMG = 4
defined?(OMG) #=> "constant"

The third line of this excerpt demonstrates that referencing an uninitialized constant normally raises a name error, so it would be normal to expect the same to happen on the fourth line, because we just saw that Ruby normally evaluates the arguments to a method. Here it just totally doesn’t, which feels kind of weird and inconsistent. It might be helpful to think of defined? as a language keyword and not a method. See also the alias method.


Back to math. Remember PEMDAS? When evaluating an arithmetic expression, we’re not supposed to just read from left to right, evaluating operations as we go; we’re supposed to prioritize some operations above others:

With this acronym memorized, children are able to evaluate complicated math expressions.

Can Ruby? Let’s see:

4 + 3 * 5   #=> 19

Well… yeah! Seems right! But let’s take a look into the order that methods are being called:

module MathLogger
  refine Fixnum do
    alias_method :original_plus, :+
    alias_method :original_times, :*

    def +(other)
      original_plus(other).tap do |sum|
        puts "#{self} + #{other} = #{sum}"
      end
    end

    def *(other)
      original_times(other).tap do |product|
        puts "#{self} * #{other} = #{product}"
      end
    end
  end
end


using MathLogger

def four
  puts 4
  4
end

def three
  puts 3
  3
end

def five
  puts 5
  5
end

puts four + three * five
# 4
# 3
# 5
# 3 * 5 = 15
# 4 + 15 = 19
# 19

Interesting! So, Ruby takes a look at four, takes a look at three, and then skips the addition, then takes a look at five, and performs the multiplication. Only then does it double back and perform the addition, inlining the product of three and five.

That’s great! And surely, if all of these operations are just methods, it will behave the same when I change it to this?

puts four.+(three).*(five)
# 4
# 3
# 4 + 3 = 7
# 5
# 7 * 5 = 35
# 35

Hm, nope. When we call the methods directly, the order of operations breaks.

I always thought it was just “syntactic sugar” that I could omit the dot when calling the + method (and its siblings) but it’s doing slightly more than just inlining the dots: it’s also, more or less, inlining the parentheses, so it looks something like this:

puts four.+(three.*(five))

How does it choose where to put the parentheses? It has a precedence table which Ruby references when deciding which operations to evaluate before others. This means that if I were able to define my own operators, I would need to be able to insert them somewhere in this hierarchy, and this hierarchy would also be cluttered with all the operators added by the gems included in my project.

Naturally, my operator would be at the top of the list.