Method Return Values in Ruby

When writing a method, you often want to return different values depending on whether some condition holds. In ruby, the standard is generally not to explicitly use the return keyword, but to simply rely on the fact that a method in ruby will return the value of the last statement in the method that gets evaluated.

This generally produces predictable results, but there are some gotchas if you’re new to ruby. Consider the following method:

def sign(number)
  if number < 0
    '-'
  elsif number > 0
    '+'
  else
    ''
  end
end

This works as expected:

sign(1)  #=> '+'
sign(-5) #=> '-'
sign(0)  #=> ''

You might think to tighten this up by using ruby’s nice one-line if statements

def sign(number)
  '-' if number < 0
  '+' if number > 0
  ''  if number == 0
end

Unfortunately, this won’t work for you:

sign(1)  #=> nil
sign(-5) #=> nil
sign(0)  #=> ''

Instead, if you use the one-line ifs, you need to use return keywords:

def sign(number)
  return '-' if number < 0
  return '+' if number > 0
  return ''  if number == 0
end

What gives? And what’s up with the nil return values when we didn’t use return? To understand this, we need to remember that ruby uses lazy evaluation for if statements, and that the value of an if statement that evaluates to false is nil.

Lazy Evaluation

Consider the following:

if 2 > 1
  puts 'foo'
elsif if 2 > 0
  puts 'bar'
else
  puts 'baz'
end

According to the ruby documentation, “Once a condition matches, either the if condition or any elsif condition, the if expression is complete and no further tests will be performed.” That means in this case that only the first conditional branch will be taken, and the others will not even be evaluated. It follows directly that if that code were at the end of a method, puts 'foo' would be the last statement evaluated in the method, and so its value (nil as it happens) would be the return value of the method.

If, however, you were to put additional statements below this if...end statement, they would of course be evaluated as usual. And this is precisely what happens when you use a series of one-line if statements, which is equivalent to

if ...
  ...
end
if ...
  ...
end
if ...
  ...
end

Even if the first of those if statements evaluates to true, the next two are evaluated as well. If that construction were the body of your method, your return value would not be the value of the statement inside the first true conditional, but instead the value of the last conditional.

A False Conditional Returns nil

Let’s return to our original ill-formed method:

def sign(number)
  '-' if number < 0
  '+' if number > 0
  ''  if number == 0
end

We know from the above that the return value of sign(1) will be the value of the statement '' if number == 0. In ruby, a conditional whose test isn’t true returns nil

puts 'hi' if 0 > 1 #=> nil

So this method will always return nil except when we pass 0 as an argument, in which case it will return ''. Adding return statements to the beginning of these lines fixes the issue simply because the return method tells ruby to immediately stop exit from the method, with the return value of whatever argument is passed to return.


Like my earlier post today, nothing revelatory here, just another example of how understanding the details of a language can explain why we use common patterns.