Kernel casts

279 views

What's the return value of the following Ruby 3 code?

nil.to_a   # => []
Array(nil) # => []

nil.to_h  # => {}
Hash(nil) # => {}

nil.to_s    # => ""
String(nil) # => ""

nil.to_f   # => 0.0
Float(nil) # => ???

The correct answer is

0.0

nil

It raises an error

0

Unlock Your Ruby Potential

Subscribe to RubyCademy and get free access to all our courses, plus hundreds of fun Ruby cards, quizzes, guides, and tutorials!

Explanation

Ruby, as a dynamic and expressive language, often surprises us with the way certain elements behave.

One such case is how nil is handled when converted to different types using Kernel methods and the associated conversions.

If you've encountered nil conversions in Ruby, you may have stumbled upon some unexpected behavior, especially involving methods like Float() and Integer()

Let's explore more deeply to understand why nil conversions can be strange, useful, or even risky in different situations.

Here's the example we begin with:

nil.to_a   # => []
Array(nil) # => []

nil.to_h  # => {}
Hash(nil) # => {}

nil.to_s    # => ""
String(nil) # => ""

nil.to_f   # => 0.0
Float(nil) # => ???

The correct answer for Float(nil) is TypeError, which may be surprising if you were expecting a conversion similar to nil.to_f, which returns 0.0.

Let’s explore why this difference exists and what other conversion methods behave similarly.

Conversion Methods That Handle nil Gracefully

Ruby provides several methods to convert nil into other types, and many of these conversions return some kind of "empty" value rather than raising an error.

Array(nil)

nil.to_a and Array(nil) both convert nil into an empty array ([]).

The method Array(arg) first tries to call to_ary on arg, and if arg does not respond to to_ary, it then calls to_a.

If neither method is available, it returns an array containing arg itself. In the case of nil, to_a is called, resulting in [].

This means that the absence of a value (nil) is translated into an empty collection, which can be useful in contexts where you want to avoid handling nil explicitly.

class MyClass; end

arg = nil
Array(arg)   # => []

arg = [1, 2, 3]
Array(arg)   # => [1, 2, 3]

arg = MyClass.new
Array(arg)   # => [#<MyClass:0x04243>]

Hash(nil)

nil.to_h and Hash(nil) similarly convert nil into an empty hash ({}).

The method Hash(arg) tries to call to_hash on arg, and if arg is nil or an empty array, it returns an empty hash ({}).

If arg cannot be converted to a hash, a TypeError is raised.

class MyClass; end

arg = nil
Hash(arg)    # => {}

arg = [[:key, :value]]
Hash(arg)    # => {:key=>:value}

arg = MyClass.new
Hash(arg)  # Raises TypeError: can't convert MyClass into Hash

String(nil)

nil.to_s and String(nil) both convert nil to an empty string ("").

The method String(arg) first tries to call to_str on arg, and if that fails, it calls to_s.

For nil, to_s returns an empty string ("").

class MyClass
  def to_s
    "I am an instance of MyClass"
  end
end

arg = nil
String(arg)  # => ""

arg = MyClass.new
String(arg)  # => "I am an instance of MyClass"

All of these conversions share a common trait: they treat nil as an "absence of value" that is substituted with a reasonable empty or neutral default.

Conversion Methods That Raise Errors for nil

However, not all conversion methods are as forgiving when dealing with nil. Consider the behavior of methods like Float() and Integer().

Float(nil) raises a TypeError with the message "can't convert nil into Float". The method Float(arg) attempts to convert arg to a float by calling to_f.

If arg is nil, a TypeError is raised because nil cannot be directly converted into a float in this context. Unlike nil.to_f, which defaults to 0.0, the Float() method refuses to guess how to convert nil and instead raises an error.

This behavior aligns with the idea of avoiding implicit assumptions about how nil should be treated when it comes to creating a number.

arg = nil
arg.to_f    # => 0.0

arg = "123.45"
Float(arg)    # => 123.45

arg = nil
Float(arg)  # => TypeError: can't convert nil into Float

Integer(nil) similarly raises a TypeError because there is no sensible default integer representation for nil.

arg = "123"
Integer(arg)   # => 123

arg = nil
Integer(arg)  # => TypeError

Both Float() and Integer() require more explicit input and prefer to raise an error if nil is provided, as they refuse to make implicit guesses about how nil should be treated as a number.

This aligns with programming principles like "explicit is better than implicit" and "errors should never pass silently."

Conclusion

Ruby's treatment of nil during conversions can sometimes feel inconsistent, but it follows a logic that balances flexibility with safety.

Unlock Your Ruby Potential

Subscribe to RubyCademy and get free access to all our courses, plus hundreds of fun Ruby cards, quizzes, guides, and tutorials!

RubyCademy ©