ebarrere
ebarrere

Reputation: 221

Implicit value of if/unless statement in ruby

I'm new to Ruby, and trying to understand why this works (it does seem to, at least according to "the Master"):

def array_of_fixnums?(array)
    true unless array.find { |item| item.class != Fixnum }
end

My concern is where the "false-ness" is coming from when the array contains non-fixnum values. Am I right to assume there is no implicit "else false" in the unless statement? In that case I assume it must be coming from the nil value returned by Enumerable#find. Is that correct?

If so, that seems a bit shaky. Might it be better to return false explicitly, like this?

array.find { |item| item.class != Fixnum } ? false : true

Is there another, better way entirely? Thanks for helping me wrap my head around this, and for any "best practice" suggestions.

Upvotes: 2

Views: 1431

Answers (3)

Cary Swoveland
Cary Swoveland

Reputation: 110725

Let's look at why true unless condition works as it does.

I believe x unless condition was provided as an esthetic improvement to x if !condition, but the two are equivalent (and, in my opinion, it is an improvement). To get rid of double-negatives, I'll just consider x if condition.

One of Ruby's basic tenets is that every statement must return a value (namely, the last expression evaluated). Therefore, x if condition must be evaluated as if condition then x else y end. But what is y? If y evaluated as true or false, x if condition would be meaningless when x is a boolean expression. To see why, consider:

w = (x == 5 if z == 6)

If w were true, we could conclude that x = 5 and z = 6, but if w were false, we wouldn't know whether z = 6 and x != 5, or z != 6 and x = 5 or x != 5. Considering that any value of y other than nil is evaluated as true or false, the only reasonable value for y is nil. That way, if w == nil is be available. Of course, this is still a problem (in other situations) when x could possibly be nil.

I would welcome clarification and elaboration. Perhaps there is also a less convoluted way of making this argument.

Upvotes: 0

numbers1311407
numbers1311407

Reputation: 34072

Your method is returning nil not because find returns nil, but because if your inline conditional does not pass, the method has no explicit return value. This would be more clear if it were not inline, consider:

def array_of_fixnums?(array)
  unless array.find { |item| item.class != Fixnum }
    return true
  end
  # otherwise don't explicitly return (implicit nil)
end

While relying on the falsiness of nil will often work, it is problematic in that it does not follow the principle of least surprise. A ? method should return true or false.

But your method has worse problems. It uses confusing logic (a double negative), and itself relies on the falsiness of nil and the truthiness of not nil, to function. Consider what happens if your method were passed [false]. Oops.

The better way would be something like:

array.all? {|n| n.is_a? Fixnum }

The reasoning is that this method does exactly what it says, plainly.

Returning a boolean explicitly, while not necessarily wrong, is superfluous and often considered bad practice. Rather consider the example, which says, in ruby speak, is every one of the values in this array a Fixnum?. The result of that expression is what the method is after; there's no reason to evaluate it then return true|false.

Upvotes: 4

hirolau
hirolau

Reputation: 13921

From the find method doc:

Passes each entry in enum to block. Returns the first for which block is not false. If no object matches, calls ifnone and returns its result when it is specified, or returns nil otherwise.

Thus, all you need is:

def array_of_fixnums?(array)
    array.find { |item| item.class != Fixnum }
end

If anything is found, that will be returned, otherwise nil will be returned, and the method will evaluate do false.

However, as the item return could be an false or nil (if any item in the list is false or nil) I would recommend that you use the .any? method instead.

def array_of_fixnums?(array)
    array.any? { |item| item.class != Fixnum }
end

Upvotes: 2

Related Questions