Handling nil in method calls
On my current project, we noticed common pattern when dealing with nil. We would often check an object to see if it was nil before calling a method on that object:
name = person ? person.name : nil
To reduce duplication, Patrick Farley and Ali Aghareza created a nil_or method which handles this. The above code becomes:
name = person.nil_or.name
The nil_or causes the expression to return nil if the target is nil. If not, the name method is called.
The code for nil_or looks like:
module ObjectExtension
def nil_or
return self unless self.nil?
Class.new do
def method_missing(sym, *args); nil; end
end.new
end
end
class Object
include ObjectExtension
end
The nil_or method returns self if self is not nil. If self is nil, it creates a new Object which eats all method calls and returns nil.
We use a fair amount of delegation on this project using forwardable, so Michael Schubert and Toby Tripp created a delegator which has the same effect. For example, you can replace this delegation:
class Person
extend Forwardable
def_delegator :@job, :title, :job_title
end
with this one:
class Person
extend Forwardable
def_delegator_or_nil :@job, :title, :job_title
end
This delegation is equivalent to this code:
class Person
def job_title
@job ? @job.title : nil
end
end
The code for def_delegator_or_nil looks like:
module ForwardableExtension
def def_delegator_or_nil(accessor, method, new_method = method)
accessor = accessor.id2name if accessor.kind_of?(Integer)
method = method.id2name if method.kind_of?(Integer)
new_method = new_method.id2name if new_method.kind_of?(Integer)
module_eval(<<-EOS, "(__FORWARDABLE_EXTENSION__)", 1)
def #{new_method}(*args, &block)
begin
if #{accessor}.nil?
nil
else
#{accessor}.__send__(:#{method}, *args,&block)
end
rescue Exception
$@.delete_if{|s| /^\\(__FORWARDABLE_EXTENSION__\\):/ =~ s} unless Forwardable::debug
Kernel::raise
end
end
EOS
end
end
module Forwardable
include ForwardableExtension
end
Comments
-
What if you extend the NilClass with method_missing so if you call person.name it will return nil if person is nil - or is this a bad idea?
-
This would also work: person = person.name if person
-
...I meant: name = person.name if person
-
@Daniel: That would conflict with the "whiny nil" behavior already built into Rails. @Paul: ahem.
-
Daniel, that would also affect nil everywhere, whereas nil_or only affects the place we use it. If we change nil, it may be hard to track down places where we get unexpected nils.
-
Hey, thanks for this post, I'm fairly new to rail and ruby. I've been wondering how others handle nil issues.
-
This is nice and DRY. In the example, you only have to mention 'person' once. This is even more important when 'person' is a more complicated expression. ///ark
-
another way without ?: person && person.name
-
I think this would cause a memory leak because it creates a new class each time #nil_or gets called. That class wouldn't be garbage collected. Instead, you should create a new object and define method_missing on its metaclass: <table class="CodeRay"><tr> <td title="click to toggle" class="line_numbers" onclick="with (this.firstChild.style) { display = (display == '') ? 'none' : '' }">
1 2 3 4 5 6 7 8
</td> <td class="code">def nil_or return self unless self.nil? o = Object.new class << o def method_missing(sym, *args); nil; end end o end
</td> </tr></table> -
There are much simpler ways to do this: http://blog.evanweaver.com/articles/2008/01/25/safe-nils-in-ruby/
-
Alek, your example only works in views. If you remove the conditional, it works everywhere, but then you've changed nil in your entire app. I like nil_or because it is more intentional. We are only changing the behavior in the place where we are using it.
-
Pat Maddox, can you give more detail? Why wouldn't the new class be garbage collected when the nil_or goes out of scope? Why is defining it on the metaclass different?