Search code examples
rubynullnull-object-pattern

Just for fun - can I write a custom NilClass for a specific use?


EDIT | Or another question, on the same object subject. Can I write my own class definition that would cause the following all to work?

o = WeirdObject.new
puts "Object o evaluates as true in boolean expressions" if o # Line never output
puts "Object o is nil?" if o.nil? # Line is output

I can do the nil? thing easy, since that's just a method. But no idea how to make it evaluate as a non-true value in a boolean expression (including just the most basic expression "o").

Original question follows...

More out of curiosity as to how much fun I can have with Ruby (1.9.2) really...

When a user is not logged in, I'd like to be able to detect it with unless @user or unless @user.nil? etc, but at the same time, I'd like to be able to call the methods of User on this object and have them return the same NilClass (ala Objective-C), since in most places this would remove the need for boilerplate if @user code, while still acting like nil in boolean expressions.

I see that you can't subclass NilClass, since it doesn't have a #new method. You can add methods directly to the instance, but it's a singleton, so this would cause issues if the only place I wanted to observe this behaviour was with non-logged-in users.

Is it possible?

What I mean, is something like this:

class CallableNil < NilClass
  def method_missing(meth, *args, &block)
    self
  end
end

user = CallableNil.new # or .singleton, or something

puts "Got here" unless user # Outputs
puts "And here" unless user.nil? # also outputs

puts user.username # no-op, returns nil

This is basically how Objective-C handles nil and it's useful in some circumstances.

I guess I could not subclass NilClass and just implement the methods that would cause it to be false in boolean expressions?

EDIT | Haha, you really break Ruby pretty badly if you redefine the top-level NilClass... it stops responding to any method call at all.

>> class NilClass
>>   def method_missing(meth, *args, &block)
>>     self
>>   end
>> end
>> 
?> nil.foo
>> 
?> 
?> puts "here"
>> quit
>> # WTF? Close dammit!

Solution

  • If your primary requirement is to be able to call methods on a nil value without raising an exception (or having to check that it's defined), ActiveSupport adds Object#try which gives you the ability to do this:

    instance = Class.method_that_returns_nil
    
    unless instance
      puts "Yes, we have no instance."
    end
    
    instance.try(:arbitrary_method)             # => nil
    instance.try(:arbitrary_method, "argument") # => nil
    

    As you've seen, screwing around with NilClass has some interesting side-effects.