Tuesday 7 June 2011

Gracefully handling Nil and Empty in Ruby

One of the nice side effects of extension methods in .NET is that you can easily handle nulls without littering your code with null checks.  

For example if you look at the following extension method that I have defined in C#:

  1.      public static bool HasElements<T>(this IEnumerable<T> collection)
  2.      {
  3.          return collection != null && collection.Count() != 0;
  4.      }

I can then use this method on any object that is an IEnumerable of T:

  1.     if (trainingEvent.Contacts.HasElements())
  2.     {
  3.         //do something
  4.     }

Which is much easier on the eye than:

  1.     if (trainingEvent.Contacts != null && trainingEvent.Contacts.Count > 0)
  2.     {
  3.         //do something
  4.     }

The reason this is possible is because an extension method in .NET is really a bit of syntactic sugar that effectively rearranges the method into a static method call, which might be equivalent to this:

  1.     if (EnumerableExtensions.HasElements(trainingEvent.Contacts))
  2.     {
  3.         // do something
  4.     }

It is all done with compiler magic.

When coding in Ruby, I found myself wanting the same experience.  All too often, I found myself writing code like this:

  1. if !lead.address.nil? && !lead.address.empty?

or this:

  1. if lead.contacts.nil? || lead.contacts.empty?

In ruby everything is an object even Nil (null in .NETspeak) and everything is open for extension.  You just redefine the class and add your own customisations.  I actually thought about extending Nil myself before telling myself that was very hacky.  I then found out that this is exactly what happens in the core extensions of the activesupport module.

In this set of extensions, the authors have extended a number of the core objects to add support for a blank? method.  According to the documentation:

An object is blank when it's false, empty or a whitespace string.  

All that I need to do is add the following line to my ~/config/initializers/requires.rb file and I get the blank? extension method over the whole project.  

  1. require 'active_support/core_ext/object'

I can then use blank? with strings:

  1. if !address.blank?

Or with arrays:

  1. if lead.contacts.blank?

That will take care of nil, empty strings, and arrays with no elements.

I get the same result for arrays, booleans and hashes.

So how is this achieved?  A quick look at the source reveals that some of the core objects are extended (hence the name stupid!).

For example the Nil class is extended to always return true for a call to blank?:

  1. class NilClass #:nodoc:
  2.   def blank?
  3.     true
  4.   end
  5. end

This will obviously elegantly handle all our nil? cases in one full swoop.  The code then opens up several other classes for extension and adds the blank? method.

For example Array is opened and a nice alias is added to enable a call to blank? to respond with empty?

  1. class Array #:nodoc:
  2.   alias_method :blank?, :empty?
  3. end

The Sting class is nicely extended with the ruby not matches operator on a simple regex:

  1. class String #:nodoc:
  2.   def blank?
  3.     self !~ /\S/
  4.   end
  5. end

But the nice bit is the extension that is added to the Object class:

  1. class Object
  2.   def blank?
  3.     respond_to?(:empty?) ? empty? : !self
  4.   end
  5. end

The respond_to? method simply checks whether the object you are sending a blank? message to has such a method.

There are other useful extensions in this module that you can find out about here.

I think this is quite nice and has tidied up my code a lot.  


If you are a seasoned Ruby developer, you probably knew this and have not bothered reading this far.




2 comments:

  1. I knew about .blank? - I just never bothered to see how it was implemented. And now you've saved me having to go look :)

    ReplyDelete
  2. It is nice blog Thank you provide important information and I am searching for the same information to save my time Ruby on Rails Online Training Hyderabad

    ReplyDelete