tl;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.
Well that was darned interesting. Thank you!
Great little writeup. Thanks Aaron!
I ran into this weirdness when RSpec was complaining that some mock object was receiving an unexpected message (to_ary). Somewhere in my code, I had an array of arrays of mocks (or, since it was a simplified unit test, probably just [[mock]]).
Related post: http://yehudakatz.com/2010/01/02/the-craziest-fing-bug-ive-ever-seen/
Seems insane that this exception is used for flow like that. I didn’t know about Array() and to_a. Having gotten into Avdi’s recent material about code confidence and exceptions, I expect that’d be one to bite me sooner or later. Great post, man.
P.S. On re-read, blurg. I’m kinda surprised Ruby tries THAT hard to get to_ary; it seems like it would have been reasonable to say “look, if you broke respond_to?() we’re not calling your to_ary()”. Especially since I don’t think ANY built-ins or stdlibs violate that expectation.
But it’s good to know def `to_ary; nil end` is the magic “NO SERIOUSLY I DON’T CONVERT” flag.
@Avdi I totally agree. If I say I don’t respond to to_ary, why is it being called?!?
This is actually quite useful. I have been using Array() a bit more often after the confident code talk at RailsConf. Simple fix, but I really don’t think Ruby should fire into method_missing for this. If respond_to? is false, that seems pretty explicit.
I am curious however if anything is actually needs this behavior in the wild. If not, we can always propose a change in future versions of Ruby. Nothing is set in stone, till too many people use it.
@Ben Hamil : http://floehopper.lighthouseapp.com/projects/22289-mocha/tickets/70
I think lot of people sooner of later comes to this problem.