Null Objects and Falsiness

Thank you to Ben Hamill for sending me a question that prompted this post.

Checking for object presence

Very often in Ruby code, we would like to execute some action only if an object is present:

def slug(title)
  if title
    title.strip.downcase.tr_s('^[a-z0-9]', '-')
  end
end
slug(" Confident Code")
confident-code
h = {}
slug(h[:missing_key])
nil

Strictly speaking, we aren’t checking for object presence here. In Ruby there is almost always an object present, but the default marker for missing data is the special nil object—the one and only instance of NilClass.

h = {}
def noop; end
Expression Result
h[:missing] nil
noop nil
if (1==2) then 'wrong' end nil

What we are really checking for is the presence of either a “falsy” object (nil or false, most likely nil), or a “truthy” object (anything other than nil or false). So in effect this is an instance of typecasing.

Switching on object type is a smell in object-oriented languages. In general, we don’t want to ask an object what it is; we want to tell it what to do and let it figure out the best way to do it based on its type. This is the essence of polymorphism.

What we need is an object to represent the special case of “do nothing on missing value”. As it happens, there is a pattern for that: Null Object. Quoting Wikipedia: “a Null Object is an object with defined neutral (‘null’) behavior”.

Ruby does not have a built-in Null Object class (NilClass doesn’t qualify). Implementation of one is trivial, however:

class NullObject
  def method_missing(*args, &block)
    nil
  end
end

Instances of NullObject will respond to any message (method call) with a no-op.

no = NullObject.new
no.foobar
nil

A more useful (and common) form of Null Object returns self from every call.

class NullObject
  def method_missing(*args, &block)
    self
  end
end

This version makes it possible to nullify arbitrary chains of method calls:

NullObject.new.foobar.baz.buz
#<NullObject:0x7f97b56214f8>

A useful accompaniment to a Null Object class is a constructor method that converts nil values into null objects, and leaves other values as-is:

def Maybe(value)
  case value
  when nil then NullObject.new
  else value
  end
end

We can also define some common conversions a la NilClass:

class NullObject
  def to_a; []; end
  def to_s; ""; end
  def to_f; 0.0; end
  def to_i; 0; end
end

With these tools in hand we can rewrite our #slug method more cleanly, and, dare I say, more confidently:

def slug(title)
  Maybe(title).strip.downcase.tr('^[0-9a-z]', '-')
end
puts slug(" Exceptional Ruby ").to_s # => "exceptional-ruby" puts slug(nil).to_s # => "" 

In some cases we may want to call methods on other objects if the maybe-null object is not null. For this case, we can define the Ruby 1.9 / ActiveSupport #tap method as a no-op for NullObject:

class NullObject
  def tap; self; end
end
Maybe(obj).tap do
  puts "Object is present!"
end

The code in the #tap block will not be executed if the object is nil.

But is it falsey?

Still, even with a null object replacing nil there may be times when we want to check whether the expected object is present or not.

user = Maybe(User.find(123))
# ... if user
  "You are logged in as #{user.name}"
else
  "You are not logged in"
end
You are logged in as

Hm, that’s not what we wanted. There is no user, but the NullObject standing in for the user is “truthy”, because it’s not false or nil.

This is particularly surprising when we are branching based on the value of a predicate:

if user.subscribed?
  "Secret subscription-only stuff!"
end
Secret subscription-only stuff!

What’s going on here? The result of the call to #subscribed? is neither true nor false; it is the NullObject instance. Which, because of Ruby’s semantics, is truthy.

As it turns out, it is not possible in Ruby to make our own objects “falsey”. We can get close:

class NullObject
  # All Ruby objects define the #nil? predicate   def nil?; true; end
end
if !user.nil?
  "You are logged in as #{user.name}"
else
  "You are not logged in"
end
You are not logged in

In a program using ActiveSupport we might also define a few more common predicates:

class NullObject
  def present?; false; end
  def empty?; true; end
end

In Ruby 1.9 we can get even closer to the ideal of user-defined falsiness by implementing the ! (negation) operator:

class NullObject
  def !; true; end
end
if !!user
  "You are logged in as #{user.name}"
else
  "You are not logged in"
end

But still the goal of being able to treat our NullObject like a common nil eludes us.

We might also try basing NullObject on NilClass or FalseClass in order to inherit their falsiness. Unfortunately this too is impossible; NilClass and FalseClass are not allocatable, meaning it is not possible to create new objects of those classes (or any derivative of them).

class NullObject < NilClass
  # NilClass has no .new defined, so we have to recreate the default   # implementation   def self.new
    o = allocate
    o.initialize
    o
  end

  def method_missing(*args, &block)
    self
  end
end

no = NullObject.new # => raises a "no allocator defined" error

We could try another tack. We could define a function to “resolve” the null object back to a nil when needed:

def Value(object)
  case object
  when NullObject then nil
  else object
  end
end

Now we when we can wrap our maybe-null object with a Value() call to get the object we need:

if Value(user)
  "You are logged in as #{user.name}"
else
  "You are not logged in"
end

If we don’t mind extending core classes we could make this an instance method instead:

class Object
  def to_value
    self
  end
end

class NullObject
  def to_value
    nil
  end
end
if user.to_value
  "You are logged in as #{user.name}"
else
  "You are not logged in"
end
You are not logged in

Chasing after the wind

Let’s take a step back. Why do we care if the value is falsey? Because we want to handle that case differently. A lot of the time, the way we want to handle the absence of an object is to do nothing, and that’s the strategy that Null Object represents.

But here we want to do something when the value is missing; we want to return a different string. Maybe the problem isn’t really one of making NullObject act falsey. Maybe this isn’t the right scenario for a Null Object at all.

Let’s instead throw together a simple Presenter implementation:

require 'delegate'

class UserPresenter < SimpleDelegator
  def login_status
    "You are logged in as #{name}"
  end
end

class NilClassPresenter < SimpleDelegator
  def login_status
    "You are not logged in"
  end
end

def Present(object)
  presenter_class = Object.const_get("#{object.class.name}Presenter")
  presenter_class.new(object)
end

(In a real app the presenters would presumably have many more methods.) Let’s see how the code looks using presenters:

user = User.find(123)
# ... Present(user).login_status
You are not logged in

Ah, there we go. Back to good old polymorphism.

Conclusion

If we’re trying to coerce a homemade object into acting falsey, we may be chasing a vain ideal. With a little thought, it is almost always possible to transform code from typecasing conditionals to duck-typed polymorphic method calls. All we have to do is remember to represent the special cases as objects in their own right, whether that be a Null Object or something else.