Introduction

Ruby has four types of logical operators: && / || / and / or, each with a different precedence.

&& / || have higher precedence as operators, with && taking precedence over ||.

and / or have low operator precedence, and there is no priority between them. In other words, they are evaluated from left to right.

These are sometimes described as being "also usable for flow control," but that statement is incorrect.

and / or are extremely powerful flow control operators. It's not an exaggeration to say they were implemented specifically for flow control (more on this later).

The suggestion that these should be used for flow control has been made for a long time, by "Ruby Good Idiom" and others, but the problem is that even those articles themselves misuse them, and as a result, they haven't become widespread. Even the official references downplay their usefulness.

In this article, I will explain these points in detail and discuss the true value of flow control operators.

Flow control syntax

As the name suggests, flow control operators are operators that perform "flow control." They combine the functions of controlling the flow and returning a value as an operator. The reason why and / or have no precedence is because they are operators for flow control. In other words, flow control operators can be rephrased as operators that control both the flow and the return value.

Since it is used for flow control, its essence is no different from control structures. In other words, using it by grouping it on a single line and nesting it is an incorrect usage that significantly reduces code readability and clarity of control flow.

If used correctly, it's best to place it at the end of a line, followed by a line break. Simply adhering to this rule alone is quite effective.

While the aforementioned "Ruby Good Idiom" uses this operator for exception handling, this is incorrect. This is because an exception is fundamentally something that goes outside the flow's control. If you're going to throw an exception, you should either throw it before entering flow control or after flow control, using a guard clause.

Examples of common control statements

When controlling the return value using common control statements, the code would look like this:

def test_method
  if a > 0 && b > 0 # Conditional comparison for calling a function.
    function(a, b) # The return value of the function will be the return value of test_method.
  else
    false # If the condition is not met, test_method will return false.
  end # If there is no else clause, the return value of test_method will be nil.
end

Enter fullscreen mode Exit fullscreen mode

It's easy to understand, but it's redundant. It doesn't have a trace of idiomatic Ruby.

Example using logical operators

When controlling return values using logical operators, the following type of code is common.

def test_method
  a > 0 && b > 0 && function(a, b)
  # Returns false when the previous conditional comparison becomes false.
  # Otherwise, the return value of the function will be the return value of test_method.
end

Enter fullscreen mode Exit fullscreen mode

While it's very compact, the code mixes two flows—conditional comparison and function call—into a single line, resulting in extremely poor code readability and clarity of control flow. This is a bad example of compressed notation, which is often seen in other languages ​​as well. It goes against Matz's philosophy.
 

Example using flow control operators

When controlling the return value using flow control operators, the code would look like this:

def test_method
  a > 0 && b > 0 and # If the condition is met, proceed to the next step in the flow.
                      # If not satisfied, return false and exit the flow.
    function(a, b) # The return value of the function will be the return value of test_method.
end

Enter fullscreen mode Exit fullscreen mode

Following the rules of flow control syntax, a flow control operator is used at the end of the line to create a new line.

The two flows—conditional comparison and function call—are visually separated, resulting in high code readability and clarity of control flow. Furthermore, there's no redundancy like in control statements. This demonstrates the extremely powerful syntax of Ruby's flow control operators.

Ruby method specifications

Ruby has a great many methods that return either a value or nil. This is intentional. In Ruby, a value (an object other than nil or false) is treated as true, and nil is treated as false. In other words, they can be evaluated with flow control operators. It can be said that this is a specification designed to control the flow and return value using flow control operators. They should be appropriately called flow control methods.

Sample code is shown below.

Example of controlling flow and return value with flow control methods

def find_employee(name)
  OFFICERS_LIST.find{|m| m.name == name } or
  EMPLOYEES_LIST.find{|m| m.name == name } or
  SECONDED_EMPLOYEES_LIST.find{|m| m.name == name } or
  nil # Does not exist.
end

Enter fullscreen mode Exit fullscreen mode

This code is a method that searches three lists in order from top to bottom and returns the employee whose name matches. The flow control method find returns value (truthy) if the employee is found, so the flow control operator or stops there, and the found value becomes the return value of the method. If it is not found, the flow moves to the next list, and if it is not found at all, it returns nil to indicate that it was not found (it also behaves as a flow control method itself).

The flow control operators and methods work together beautifully, highlighting a simple flow structure completely free of unnecessary code. This is the world Ruby aimed for. This is the very essence of Matz's philosophy.

If you're concerned about the duplication in the name check part of the above code, you can simply convert the closure into a Proc and pass that instead. That's a very Ruby-like solution, after all.

Some of you might be thinking, "Isn't the final nil unnecessary?" And you'd be right, because the final find also returns either a value or nil, so the result wouldn't change even if it were omitted. However, it's necessary for visualizing the flow. It would be a waste to enhance the clarity of control flow with flow control operators only to hide it at the end. That's why I put nil here.

How to use and / or

As in the first flow control example, and serves to connect the conditional expression with the next flow. It's helpful to remember that it's an operator that advances the flow if the condition is met.

or serves to connect flow control methods, as in the last example. It's helpful to remember that it's an operator that advances the flow if no value is found (if nil is returned) by the flow control method.

summary

I hope this article has helped you reaffirm just how beautifully designed Ruby is. The elegant flow control using flow control operators, and the overwhelming readability when used in conjunction with flow control methods—Ruby is a human-centered language designed to make programming enjoyable.

Along with the previously posted guide on "how to correctly remember unless and until", I hope many Rubyists will find this useful.

Related: [Ruby] How to Truly Understand unless and until

bonus

Basic Guidelines for Flow Partitioning

As some of you may have noticed from this article and the sample code in the aforementioned unless article, negation operators (! and not) are not used at all, except in the bad examples. This is not because I chose those conditions and only used them. In fact, the inclusion of negation operators in conditional expressions is evidence that the flow has not been properly divided.

A clear code example is shown below.

return if !obj || obj.empty? # Commonly seen guard clause.

Enter fullscreen mode Exit fullscreen mode

The example above is a typical guard clause that any Ruby programmer has probably written at some point. However, upon closer inspection, it contains two interwoven flows. If these flows are properly separated, it looks like this:

return unless obj    # Whitelist (except nil).
return if obj.empty? # Blacklist If the content is empty, do not allow it.

Enter fullscreen mode Exit fullscreen mode

If flows are not mixed in this way, negation operators will basically not be mixed in. Therefore, if a conditional expression contains a negation operator, you should first check whether flows are mixed in. If conditional expressions and function calls are mixed in, as in this article, or if the whitelist and blacklist guard clauses are combined into a single guard clause, as in the example above, simply separating the flows in that section should dramatically improve the readability of the code and clarity of control flow.

Of course, it's impossible to eliminate them completely. For example, there are cases like !ary.include?(0) where there's nothing you can do. Implementing an exclude? method is an option, but this level of negation doesn't significantly reduce readability, so it's acceptable. The important thing isn't to eliminate negation operators. It's simply about improving readability and clarity of control flow through proper flow division.

Your past self is a stranger

This emphasis on readability might sometimes come across as uncouth or overly intrusive. However, those who have been programming for a long time will likely understand the experience of looking at their own code years or months later, after being away from the field for a while, and finding it incomprehensible—like looking at spaghetti code written by someone else.

Adding annotations with comments is fine, but doing so for every single line of code is redundant and looks bad. It's far better to separate the flow and design the code to speak for itself.

This is so important I'll say it twice: "Your past self is a stranger." Paradoxically, this means your future self is also a stranger. Even if you're developing a project on your own, or perhaps especially because you're developing it on your own, write code that won't confuse your future self. Because the consequences of that will undoubtedly fall back on you.

Related Links