TIL: It's OK to return nil from to_ary
2011-06-28 @ 16:08tl;dr: You can return nil from to_ary
and to_a
.
Today I discovered that it's OK for to_ary
to return nil. Ruby spec tests this behavior, and the
ruby implementation supports it. But why would you want to implement this?
Array#flatten and to_ary
Consider the following code:
class Item def respond_to?(name, visibility = false) p "respond to: #{name}" false # we don't respond to anything!! end def method_missing(name, *args) p "method missing: #{name}" super # if something? # do something else end end [[Item.new]].flatten
In Ruby 1.9, flatten
will actually call to_ary
on each of the items in the collection. If the method table for this object contains to_ary
, Ruby will call to_ary
, and if method_missing
is implemented, it will call method_missing
regardless of how your respond_to?
returns. The reason ruby will call method_missing
is because it could implement to_ary
. (As for why it ignores the return value of respond_to?
, I don't know)
Naturally, if we were to call to_ary
directly on this object it would raise a NoMethodError
exception. When calling flatten, ruby will swallow the exception. If we enable $DEBUG
by running ruby with -d
, we can see the exception:
[aaron@higgins ~]$ ruby -d test.rb
Exception `LoadError' at /Users/aaron/.local/lib/ruby/site_ruby/1.9.1/rubygems.rb:1215 - cannot load such file -- rubygems/defaults/operating_system
Exception `LoadError' at /Users/aaron/.local/lib/ruby/site_ruby/1.9.1/rubygems.rb:1224 - cannot load such file -- rubygems/defaults/ruby
"method missing: to_ary"
Exception `NoMethodError' at test.rb:9 - undefined method `to_ary' for #<Item:0x00000101079e80>
"respond to: to_ary"
[aaron@higgins ~]$
Dispatching to method_missing
can be expensive, and using exceptions for flow control even more problematic. The way we can get around this issue is by implementing to_ary
and having it return nil:
class Item def respond_to?(name, visibility = false) p "respond to: #{name}" false # we don't respond to anything!! end def method_missing(name, *args) p "method missing: #{name}" super # if something? # do something else end private def to_ary nil end end [[Item.new]].flatten
Run the code again with -d
, and you'll see no more calls to respond_to?
, no more calls to method_missing
, and no more exceptions raised:
[aaron@higgins ~]$ ruby -d test.rb
Exception `LoadError' at /Users/aaron/.local/lib/ruby/site_ruby/1.9.1/rubygems.rb:1215 - cannot load such file -- rubygems/defaults/operating_system
Exception `LoadError' at /Users/aaron/.local/lib/ruby/site_ruby/1.9.1/rubygems.rb:1224 - cannot load such file -- rubygems/defaults/ruby
[aaron@higgins ~]$
Array() and to_a
The Array() function exhibits a similar behavior, but with the to_a
method. Try this same code, but rather than using Array#flatten
, do this:
Array(Item.new)
We can fix the error produced by this code with a slight change to our class:
class Item def respond_to?(name, visibility = false) p "respond to: #{name}" false # we don't respond to anything!! end def method_missing(name, *args) p "method missing: #{name}" super # if something? # do something else end private def to_ary nil end alias :to_a :to_ary end [[Item.new]].flatten Array(Item.new)
Hmmmm
Are warnings annoying? Yes. Is this strange behavior? I tend to think so. But if I'm forced to deal with a class the implements method_missing
, I'd like to reduce the number of calls to method_missing
.
Anyway. I hope you found this informative. Have a Happy Tuesday!!!!
<3 <3 <3 <3
Small Side Note
The reason there are exceptions coming from rubygems when -d
is enabled is because rubygems attempts to require files that are not shipped with rubygems. These files are for packagers to provide. For example someone packaging rubygems for debian, may need to do customizations and those files are where that happens.