Friday, August 31, 2007

Cheap Tricks V - Pwning Nil

A Healthy Respect of Null

I recently traveled from the over-populated World of Java into the beautifully rustic Land of (professional) Ruby. Like any traveler, I had a few companions (ideologies, habits and conventions) which kept me safe and sound in Java's harrowing landscape. But when I left Java for newer pastures some of those companions trailed along. One of them is named Scary Null.

In Java - null is not an object - just ask it:
null instanceof Object
will be false. Worse yet, attempting to use it for anything other than a place-holder and he'll throw his even scarier friend NullPointerException at you. Over the years I learned a healthy respect for null. He has one and only one job: to hold the place of something. How dare I use null in a capacity for Which He Was Not Intended!

A Healthy Disrespect of Nil

In Ruby Land, however, he has a cousin: nil. But nil is not a fearful beast like null. Nil is a charming little scamp, a first class object that does her best to help you out. This took some getting used to for me. I'm so used to writing code like this in Java:
int i = str == null ? 0 : Integer.parseInt(str);
that it almost feels awkward to do it the Ruby way:
i = str.to_i
In the back of my mind I think: "Eric, what are you doing! What if 'str' is nil? I'll get an exception!" Au contraire. Since nil is a real object, it doesn't throw exceptions - nil is an object of the type NilClass which implements plenty of useful methods, like:

  • &

  • |

  • ^

  • nil?

  • to_a

  • to_f

  • to_i

  • to_s

  • to_yaml

Let's take the first three: &, | and ^. Since those are respectively "and", "or" and "xor", you can imagine what nil represents: false. That makes code like this possible:
c is true. No exception. If I was smacked upside the head with a board and the ensuing brain damage caused me to want nil to throw an exception in this situation, I could always override the method in nil
The other methods are equally kind. nil.to_s (to string) returns "" (empty string), nil.to_a (to array) returns [] (empty array), and my favorite nil.nil? return true - and is the only object that does.

Bring on the Fun

Nil is fun, but so what? If I attempt to use a method that doesn't exist, I'll still get an exception! True. However, since we know that nil is a real object, and since Ruby can override object implementations on the fly (weee!) we can also add methods to nil.

Consider the following code, that iterates until a string is constructed that is over 10 character long, then returns the string:
It outputs:
la
lala
lalala
lalalala
lalalalala
lalalalalala
Did you see something wrong with that picture? Not if you are from the World of Java. Initialize a variable, iterate, append and finally return the value on the correct conditions. But:

Why declare a variable that we only use inside the loop and return? Why indeed. Let's try it again without declaring the variable.We get an error :(.
NoMethodError: You have a nil object when you didn't expect it!
You might have expected an instance of Array.
The error occurred while evaluating nil.+
Oh - it looks like Ruby requires us to define variables first. And since str += "la" is the same as str = nil + "la" when str is not yet defined, it makes sense that it complains.

But wait! Re-read the last sentence. Of course! Ruby thinks that str is nil, and is attempting to append the string "la" to it. So what would happen if the nil.+ method was declared? What if it just returned whatever was passed to it?
def nil.+(o)
o
end
Now we're cooking with gas! Try and run the above code now. It works(!) since our nil.+ method just set str to "la". For each successive loop, str just uses the standard string append method instead.

There are a few methods I like to add to nil. Just create a file named "safe_nil.rb" in your library, and add it like any requires.
Now you can safely do things like this:instead of

11 comments:

jimbojw said...

Very clever! If I may, I'd like to suggest these as well:

# Behaves like an empty array when pushed
def nil.push(o); [o]; end

# Behaves like an empty string when appended to
def nil.<<(o); o; end

Eric said...

I was attempting to avoid anything that would represent that the nil had any state.

"a = nil + 1" makes sense that "a" would be set to "1", "nil" is just an alias for "0".

But "nil << 1"? Seems misleading. Maybe I just don't get it? That's possible.

Anonymous said...

nil.to_i is equal to 0?
Why not -1?
Sorry, but that sounds to me like provoking all kinds of hidden errors.

I'm glad that I'm still working in Java, where we have a real null value.

Dejan Dimic said...

th eimage http://bp3.blogger.com/_E3_4LXYT1LI/RtZHLnSBZ_I/AAAAAAAAAEo/V8ctJ6Devn4/s1600-h/ruby.png is brilliant :-)

JAlexoid said...

OMG!

Clever but error prone.....

Null or Nil are markers of UNKNOWN.
SQL handles null values the best way...
Java's null's are nonexistent pointers, and nothing else. A legacy from C and 0 pointer(NULL pointer).

And BTW nil == 0 must be false, and so must be nil.to_i == 0. 0 is an integer why the inconsistency?

Sure you are just hacking, but this may be a really destructive idea...

BTW you kick the cup, witch way are you kicking? Just like everything else in Ruby 1000 ways to do the same thing...

Eric said...

Anonymous:
I never set nil.to_i == 0. That is a built-in value of NilClass in Ruby. All I wanted to note was that if Ruby is going to support the idea of nil as 0/false -- why not extend it?

JAlexoid:
Nil will always be a marker of "unknown" in Ruby, since it is the obly object that returns "true" for "nil?".

If you didn't notice he's kicking a Java cup - representative, see? The bad habits Java people (myself included) bring into Ruby have to go.

JAlexoid said...

Eric:
See my problem is that unknown cannot be 0. Can't the "nil?" method be overridden?
But basically I like the idea, that null's and nil's are not correctly defined for most languages. SQL is closest and Ruby is on the right track.

I know what the cup represents :)

J.J. said...

Who ever said that Ruby's nil is "unknown"??
Wrong. In Ruby, nil is nil. The value of nil is nil. (no value, so it is written as nil)
If you convert it to an integer, it's value will be 0
Makes sense, since 0 is the value of nothing!
Convert to a float, same, 0.0
convert to a string, "", empty string object!
to array? [] empty array object!!
These things make sense. To Matz. And to me!
On top of that, they're very convenient.
Not convenient for certain forms of mathematics ? or for your sense of logic? Then use a different class or redefine it!
Beauty of Ruby.

Kesor said...

Yikes. Some scary ruby in this post.
With methods like that for your nil objects - one can soon find himself chasing bugs that don't normally exist.

Nil is nil. It is also completely incorrect to say that nil is false.

nil.class # => NilClass
false.class # => FalseClass
true.class # => TrueClass

Yes a nil when considered in an integer context might be a 0, and when considered in a boolean context might be a false. But nil is none of these.

nil is an instance of the NilClass, and there are methods to that class that conveniently allow to create other objects. nil.to_i will will create a Fixnum object with the value of 0, it really does not mean that nil is 0, when using nil.to_f you get a Float instance with the value of 0.0, and nil is not a float either.

When someone says that nil is 0, or that nil is false, or that nil is "", or that nil is 0.0 - that is completely incorrect. Perhaps in C/C++ NULL was defined as 0 or -1 and vice-versa. In Ruby, nil is nil.

James said...

Regarding jimbojw's methods. I think they make complete sense. nil.to_a returns [], so nil.push(o) should behave like [].push(o).

Ditto for strings. I love it!!

For all of the people worried about nil, keep in mind that you wouldn't want to test nil == 0, but you may want to test that a == 0, when a is meant to be an int, but is really undefined. I think Ruby's implementation for nil is very nice. It allows the same behaviour as SQL, where there is a nil object that is not true or false but represents the absence of a value. nil != false, and false.nil? is false. It is it's own unique value while still being a real object and not allowing for a NPE. To anonymous: Java's null value isn't really a real value. That is why you get a null pointer exception.

Anonymous said...

It looks great for me! But I think you can go even a step further. Different classes can expose different specialized behaviours on specific methods.

For this reason, with the help of Java's static type checking, you should be able to deal with NULL as particular values for every class, instead of a particular built-in NilClass object.

Explained in: Null variables invocations