Kurt Stephens

Nerd Up!

Ruby : Touching The Obj-C Void : nil is nil

Kurt on Sat, 2007-08-04 03:42.

A long time ago, in Objective-C on the NeXT, one could often remove nil checks, because all messages to nil would immediately return nil (or 0 depending on the caller’s method signature).

How many times have we seen this in Ruby?:

def foo
  bar && bar.baz && bar.baz.caz("x")
end

Or even worse, avoiding redundant execution?:

def foo
  (temp = @bar) &&
  (temp = temp.baz) && 
  temp.caz("x")
end

In Objective-C this could be written as:

- foo {
  return [[bar baz] caz: "x"];
}

So in Ruby:

class ::NilClass
  def method_missing(*args)
    nil
  end
end

@bar = nil
def foo
  @bar.baz.caz("x")
end

foo
# => nil

Assuming that most of the time bar is not nil, NilClass#method_missing => nil makes for cleaner code that also runs faster than checking for nil along the way.

An additional benefit is that nil can also be used as an immutable empty collection sink by defining NilClass#size => 0, NilClass#empty? => true, etc.

Obviously, it breaks code that expects exceptions to be thrown for messages to nil.

Introduce a method that explicitly checks for nil:

module ::Kernel
  def not_nil; self; end
end

class ::NilClass
  def not_nil; raise("not_nil failed"); end
end

@bar = nil
def foo
  @bar.baz.caz("x").not_nil
end

foo 
# => RuntimeError: not_nil failed

Comments?

links: Kurt's blog | add new comment | 529 reads

Love it


This definitely results in more succinct code. The only downside other than breaking code that depend on nil’s throwing exceptions would be isolating the points in the method chains that result in the origiNil values.

I definitely agree that code like this is a bad idiom that I have seen all over the place:

def foo; bar && bar.baz && bar.baz.caz("x"); end

And code like this is equally bad in that it over-eagerly traps errors you might want to know about otherwise:

def foo; bar.baz.caz('x') rescue nil; end

I had considered implementing a proxy for a while, but it felt at least as visually expensive and probably computationally too:

def foo; bar.if.baz.caz('x').else(nil); end

There’s a nice sense of simplicity in your solution. It clashes with the WhinyNil approach of ActiveSupport/Rails – though I see that with good test coverage the impact on losing warnings caused by method calls on nil might be negligible.

Cool idea.