Ruby's 'unless X' is processed differently than 'if !X'!

During a code review at work today, I suggested the usage of if a.exclude?(b) over unless a.include?(b) in a Rails project. I said that both of those expressions would be interpreted as if !a.include?(b).

I was wrong.

While unless X is resultantly equal to if !X, the actual instructions generated behind the scenes are not the same.

I discovered this while trying to learn the difference in implementation of the both. Unfortunately for me, the source code did not make the definitions transparent and it would have been quite time consuming for me to dive into Ruby’s source.

For those who are unfamiliar with it, RubyVM::InstructionSequence is helpful in determining what instructions are sent to the Ruby Virtual Machine when it is executing code. While those instructions are not exactly what our CPU receives, they are an excellent breakdown of the precise overall (or ‘big-picture’) steps that are carried out. (You can read more about Ruby’s code execution over here.)

So far, so good. The “branchunless” instruction was a little surprise. Apparently Ruby uses “branchunless” instead of something like “branchif”.

You can think of “branchunless” as a conditional-jump; the condition is the output of the previous instruction. If the previous instruction is true, there is no jump; if the previous instruction is false, there is a jump; hence the “unless” in “branchunless”.

As you can see, I was wrong - the instructions were identical to #2 rather than #3. Eventually, the output is the same but it was interesting to see that “unless X then a else b” translated to “if X then b else a” rather than “if not X then a else b”. Pretty much like a “reverse ternary operator”, if I were to badly name it.

I found this behavaiour interesting because Ruby’s implementation skips the negation step thereby saving time which would otherwise have been spent on using a “branchunless”. That is excellent. And it makes me feel slightly silly when I look back on what I thought was going on.

5. “unless !true; ‘iztroo’; else; ‘izfalz’; end”

Why? Because I had to. I had to confirm a new prediction. If was right, the instructions would reverse the order of “blocks” i.e. “iztroo” and “izfalz”, then use a “branchunless” instruction which depended on an “opt_not” instruction.

It was then time to revisit the topic that started it all: unless a.include?(b) or if a.exclude?(b).

First, I should explain that exclude? is implemented as !include?. Therefore the decision boils down to unless a.include?(b) or if !a.include?(b).

My instructions-research clearly shows that the first one is better in terms of speed. No negation steps will be added therefore the result will be yielded faster.

But practically, the cost of that additional instruction is negligible in modern CPUs for most scenarios. Furthermore, in my opinion, the latter is reader-friendlier i.e. if a.exclude?(b). To compromise readability over such an insignificant speed improvement would be ill advised.

Having settled on if a.exclude?(b) in my mind, I enjoyed a much deserved cup of hot chocolate.