Search code examples
rubymetaprogrammingnull-object-pattern

Ruby nil-like object


How can I create an Object in ruby that will be evaluated to false in logical expressions similar to nil?

My intention is to enable nested calls on other Objects where somewhere half way down the chain a value would normally be nil, but allow all the calls to continue - returning my nil-like object instead of nil itself. The object will return itself in response to any received messages that it does not know how to handle and I anticipate that I will need to implement some override methods such as nil?.

For example:

fizz.buzz.foo.bar

If the buzz property of fizz was not available I would return my nil-like object, which would accept calls all the way down to bar returning itself. Ultimately, the statement above should evaluate to false.

Edit:

Based on all the great answers below I have come up with the following:

class NilClass
  attr_accessor :forgiving
  def method_missing(name, *args, &block)
    return self if @forgiving
    super
  end
  def forgive
    @forgiving = true
    yield if block_given?
    @forgiving = false
  end
end

This allows for some dastardly tricks like so:

nil.forgiving {
    hash = {}
    value = hash[:key].i.dont.care.that.you.dont.exist
    if value.nil?
        # great, we found out without checking all its parents too
    else
        # got the value without checking its parents, yaldi
    end
}

Obviously you could wrap this block up transparently inside of some function call/class/module/wherever.


Solution

  • This is a pretty long answer with a bunch of ideas and code samples of how to approach the problem.

    try

    Rails has a try method that let's you program like that. This is kind of how it's implemented:

    class Object
      def try(*args, &b)
        __send__(*a, &b)
      end
    end
    
    class NilClass        # NilClass is the class of the nil singleton object
      def try(*args)
        nil
      end
    end
    

    You can program with it like this:

    fizz.try(:buzz).try(:foo).try(:bar)
    

    You could conceivably modify this to work a little differently to support a more elegant API:

    class Object
      def try(*args)
        if args.length > 0
          method = args.shift         # get the first method
          __send__(method).try(*args) # Call `try` recursively on the result method
        else
          self                        # No more methods in chain return result
        end
      end
    end
    # And keep NilClass same as above
    

    Then you could do:

    fizz.try(:buzz, :foo, :bar)
    

    andand

    andand uses a more nefarious technique, hacking the fact that you can't directly instantiate NilClass subclasses:

    class Object
      def andand
        if self
          self
        else               # this branch is chosen if `self.nil? or self == false`
          Mock.new(self)   # might want to modify if you have useful methods on false
        end
      end
    end
    
    class Mock < BasicObject
      def initialize(me)
        super()
        @me = me
      end
      def method_missing(*args)  # if any method is called return the original object
        @me
      end
    end
    

    This allows you to program this way:

    fizz.andand.buzz.andand.foo.andand.bar
    

    Combine with some fancy rewriting

    Again you could expand on this technique:

    class Object
      def method_missing(m, *args, &blk)        # `m` is the name of the method
        if m[0] == '_' and respond_to? m[1..-1] # if it starts with '_' and the object
          Mock.new(self.send(m[1..-1]))         # responds to the rest wrap it.
        else                                    # otherwise throw exception or use
          super                                 # object specific method_missing
        end
      end
    end
    
    class Mock < BasicObject
      def initialize(me)
        super()
        @me = me
      end
      def method_missing(m, *args, &blk)
        if m[-1] == '_'  # If method ends with '_'
          # If @me isn't nil call m without final '_' and return its result.
          # If @me is nil then return `nil`.
          @me.send(m[0...-1], *args, &blk) if @me 
        else 
          @me = @me.send(m, *args, &blk) if @me # Otherwise call method on `@me` and
          self                                  # store result then return mock.
        end
      end
    end
    

    To explain what's going on: when you call an underscored method you trigger mock mode, the result of _meth is wrapped automatically in a Mock object. Anytime you call a method on that mock it checks whether its not holding a nil and then forwards your method to that object (here stored in the @me variable). The mock then replaces the original object with the result of your function call. When you call meth_ it ends mock mode and returns the actual return value of meth.

    This allows for an api like this (I used underscores, but you could use really anything):

    fizz._buzz.foo.bum.yum.bar_
    

    Brutal monkey-patching approach

    This is really quite nasty, but it allows for an elegant API and doesn't necessarily screw up error reporting in your whole app:

    class NilClass
      attr_accessor :complain
      def method_missing(*args)
        if @complain
          super
        else
          self
        end
      end
    end
    nil.complain = true
    

    Use like this:

    nil.complain = false
    fizz.buzz.foo.bar
    nil.complain = true