Handling nil in method calls

written by paul on February 17th, 2008 @ 12:14 PM

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

  • Daniel on 17 Feb 13:54

    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?
  • Geoff B on 17 Feb 14:12

    This would also work: person = person.name if person
  • Geoff B on 17 Feb 14:15

    ...I meant: name = person.name if person
  • Toby on 17 Feb 14:40

    @Daniel: That would conflict with the "whiny nil" behavior already built into Rails. @Paul: ahem.
  • Paul Gross on 17 Feb 15:07

    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.
  • andycamp on 17 Feb 22:13

    Hey, thanks for this post, I'm fairly new to rail and ruby. I've been wondering how others handle nil issues.
  • Mark Wilden on 17 Feb 22:15

    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
  • runmen on 18 Feb 03:42

    another way without ?: person && person.name
  • Pat Maddox on 18 Feb 12:04

    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>
  • Alek on 18 Feb 12:16

    There are much simpler ways to do this: http://blog.evanweaver.com/articles/2008/01/25/safe-nils-in-ruby/
  • Paul Gross on 18 Feb 19:26

    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.
  • Paul Gross on 18 Feb 19:31

    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?

Post a comment

Options:

Size

Colors