Precedence & Whitespacing In Ruby

Mar 30 · 7 min read

Let’s consider the following code:

Fig 1: this is not pretty, but it technically works.

While disturbing, this code is 100% functional. It divides six by two and adds the result to three, then stores the result into sum. It does this while smeared over five lines, and with nineteen spaces sloshed in for good measure.

On the other hand, this code breaks:

Fig 2: while prettier, this doesn’t work.

An experienced coder will be nodding along at this point. You completed the sum statement, then went to a new line, declared +6, then went to line three where you began a regex statement with /2. Thus: unterminated regexp meets end of file.

These examples teach us something: Ruby has rules for when it will and will not cross into a new line to look for information, and these rules can have unexpected consequences. Without a proper understanding of how Ruby works at a fundamental level you run the risk of breaking your program — or at the very least bogging it down with excessive parenthetical statements.

In a dynamic programming language like Ruby, the mark of a good programmer is the capacity to trust their code, and understand implicitly what it is doing. With that in mind, let’s talk about whitespace and precedence.


Whitespace

Fig 3: a disturbing way of calling a function.

Here, we see another example of a function call that works, but isn’t pretty. In fact, most people would tell you that this code belongs in a dumpster purely on aesthetics. However, it’s crucial to note that it does work. The question is, why?

Each piece of code promises to do something. Code, in fact, can be thought of as a series of promises; as a result, there is nothing code hates more than a broken promise.

So how does this mutant code work? Well, times promises that there will be a function call with two arguments. It gets the first of those in 5. Then, it sees the comma, a second promise that says, “Hey, more is on the way!”

The code trusts you, and moves ahead, looking all the way to the 3 spaced half way into the next line, then puts it together. What if, on the other hand, you didn’t have the comma?

Fig 4: it broke!

We error out. Even though we’re using all the same characters, putting the comma on the new line caused the code to not look to that line for more information. As a result, the promise we made to the function failed— we told it we would supply two variables, and only gave one.

This same principle can be observed in Fig 1, our functional ugly code duckling. No line ends with a complete statement — instead, each ends with a promise that more information is coming. Since the information does show up eventually, Ruby lets us get away with it (even if our fellow programmers might not).

Okay, so we know we can make our code look stupid, but what practical use it that? Well, you’ve probably seen this before:

Fig 5: whitespace and promises allows multiline declarations of variables and functions.

This hash declaration is spread out over five lines using whitespacing and commas to ensure that the code is aware that there will be another variable coming. It is legible, clean, and functional. In this instance we were able to use whitespacing to spread the information out to help others (and our future selves) interpret our code.


Precendence

Fig 6: table courtesy of Techtopia: https://www.techotopia.com/index.php/Ruby_Operator_Precedence

All this talk of whitespace provides a natural transition into our next topic: precedence. Precedence is a rule in programming which states that certain operations will occur before others. Consider this code:

Fig 7: PEMDAS

Now, a person will look at this and parse it. First we multiply, then we divide, then we add. 3 * 2 becomes 6, 6/2 becomes 3, 1 + 3 + 3 becomes 7. Simple! Except in order for this to work, the code isn’t being read right to left. It’s being read middle to outside, jumping all over!

This happens because of precedence, or the idea that certain pieces of code should be evaluated before others. This allows us to get instances of code where the output does not match what might assume. For example:

Fig 8: humans don’t read like computers

Many coders first instinct is to assume that the two and three are fed into the function, which evaluates to eight. Then we would add one to get nine, right? Actually, no. Function calls in Ruby have the lowest possible precedence. On the other hand, unary addition has the third highest; as a result, in spite of how it looks, this is evaluated the same way as pow(2, 3+1) would be. As noted before, white space is not important when evaluating a statement as a whole. All that matters is the promise provided by the + sign, and that promise has priority over the one made by the pow function.

As another case study, newer programmers in Ruby often run into an issue like this when attempting to instance functions inside an object. For instance:

Fig 9: unexpected consequences of precedence.

Let’s break this down. We’ve got a very basic class that is initialized with two numbers, complete with a method that adds them together and stores them in one of the two. We have explicitly defined both getters and setters (just to hammer home the point that they are indeed there). Then, we initialize an instance of HoldsNumbers, giving it a = 2and b = 3. We call addNumbers, and…

Undefined method ‘+’ for nil:NilClass

How can that be? Not only did we define a as a variable, it’s a function on top of that! It’s double-declared! How did we miss both of those? The answer, as you might expect, is precedence.

Look at Figure 6. It shows all the ruby operators in order of precedence, and who is that right at the top? It’s our old friend =, the assignment operator. So, = is a function with reasonable precedence. Long before any function you’ve declared, or any variable, your code will call assignment. In this line of code, it’s the first thing it notices. And what does it do? It tries to assign a value to new variable under the provided name, a.

When the code attempts to set a as a variable, it gives it the default value of nil. Then, it tries to add b (which properly calls the b function) to nil. And what do we get?

undefined method `+’ for nil:NilClass (NoMethodError)

Because precedence.

Let’s look at another example that can trip up programmers of all experience levels.

Fig 10: do/end and {} are NOT the same

There is a common lie circulating the internet and message boards that do/end and {} are the same thing in Ruby. The difference between the two is subtle, but it does exist and can cause errors if you don’t know what to look for.

In the above example, we are attempting to print the output of a simple map on an array. In our first attempt we use a do/end, and in our second we use curly braces. The braces work like a charm. The do/end on the other hand returns an Enumerator object. In fact, the first returns an enumerator regardless of what iterator function is called on the array. Why?

do/end blocks have the lowest possible precedence. As a result, the block has less priority than the function puts. puts runs, finds [1,2,3].map, a valid target with a to_s method. It prints it, “it” being the enumerator object map. The block is thrown out altogether — it isn’t run at all. If map was replaced with delete_if or any other destructive iterator it would fail to alter the target array at all.

It’s worth noting that precedence can be overcome with parenthesis. In much the same way that we can manipulate a mathematical statement to ignore order or operations ((1 + 3) * 4 = 16 , not 13), you can use them in code to much the same effect. Parenthetical statements will receive high precedence, forcing them to be evaluated first.


In Closing

Whitespacing and precedence in combination can cause unexpected problems, but also be a powerful tool for legibility and flexibility for how your code is executed. Keeping both in mind will allow a coder to identify and eliminate bugs much more quickly, and keep their code clean.


BONUS

If you want more to think about, consider this:

Instead of returning the array, as the mapped version did, this one ran puts, the statement which only appears once, three times. Why? What is going on here? Use your knowledge of precedence and whitespace to determine the cause of this odd output!


Thank you for your time, and I hope you found this informative! If you have any feedback or corrections, feel free to leave a comment or message me directly and I’ll try to get back to you as quick as I can.


Rylan Bauermeister

Written by

Software engineer and writer who likes weird edge cases and talking about the world.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade