Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ruby nil-like object

How can I create an Object in ruby that will be evaluated to false in logical expressions similar to nil?

My intention is to enable nested calls on other Objects where somewhere half way down the chain a value would normally be nil, but allow all the calls to continue - returning my nil-like object instead of nil itself. The object will return itself in response to any received messages that it does not know how to handle and I anticipate that I will need to implement some override methods such as nil?.

For example:

fizz.buzz.foo.bar

If the buzz property of fizz was not available I would return my nil-like object, which would accept calls all the way down to bar returning itself. Ultimately, the statement above should evaluate to false.

Edit:

Based on all the great answers below I have come up with the following:

class NilClass
  attr_accessor :forgiving
  def method_missing(name, *args, &block)
    return self if @forgiving
    super
  end
  def forgive
    @forgiving = true
    yield if block_given?
    @forgiving = false
  end
end

This allows for some dastardly tricks like so:

nil.forgiving {
    hash = {}
    value = hash[:key].i.dont.care.that.you.dont.exist
    if value.nil?
        # great, we found out without checking all its parents too
    else
        # got the value without checking its parents, yaldi
    end
}

Obviously you could wrap this block up transparently inside of some function call/class/module/wherever.

like image 481
Finbarr Avatar asked Jan 07 '12 00:01

Finbarr


People also ask

Is nil an object in Ruby?

In Ruby, nil is—you've guessed it—an object. It's the single instance of the NilClass class. Since nil in Ruby is just an object like virtually anything else, this means that handling it is not a special case.

Is it better to use OBJ nil or obj == nil Ruby?

The actual results showed that using obj as a nil check is the fastest in all cases. obj is consistently faster by 30% or more than checking obj. nil? . Surprisingly, obj performs about 3-4 times as fast as variations on obj == nil , for which there seems to be a punishing performance penalty.

What does != nil in Ruby mean?

In Ruby, nil is a special value that denotes the absence of any value. Nil is an object of NilClass. nil is Ruby's way of referring to nothing or void.

Why does Ruby use nil instead of null?

nil is an Object, NULL is a memory pointer Sadly, when this happens, Ruby developers are confusing a simple little Ruby object for something that's usually radically different in “blub” language. Often, this other thing is a memory pointer, sometimes called NULL, which traditionally has the value 0.


2 Answers

This is a pretty long answer with a bunch of ideas and code samples of how to approach the problem.

try

Rails has a try method that let's you program like that. This is kind of how it's implemented:

class Object
  def try(*args, &b)
    __send__(*a, &b)
  end
end

class NilClass        # NilClass is the class of the nil singleton object
  def try(*args)
    nil
  end
end

You can program with it like this:

fizz.try(:buzz).try(:foo).try(:bar)

You could conceivably modify this to work a little differently to support a more elegant API:

class Object
  def try(*args)
    if args.length > 0
      method = args.shift         # get the first method
      __send__(method).try(*args) # Call `try` recursively on the result method
    else
      self                        # No more methods in chain return result
    end
  end
end
# And keep NilClass same as above

Then you could do:

fizz.try(:buzz, :foo, :bar)

andand

andand uses a more nefarious technique, hacking the fact that you can't directly instantiate NilClass subclasses:

class Object
  def andand
    if self
      self
    else               # this branch is chosen if `self.nil? or self == false`
      Mock.new(self)   # might want to modify if you have useful methods on false
    end
  end
end

class Mock < BasicObject
  def initialize(me)
    super()
    @me = me
  end
  def method_missing(*args)  # if any method is called return the original object
    @me
  end
end

This allows you to program this way:

fizz.andand.buzz.andand.foo.andand.bar

Combine with some fancy rewriting

Again you could expand on this technique:

class Object
  def method_missing(m, *args, &blk)        # `m` is the name of the method
    if m[0] == '_' and respond_to? m[1..-1] # if it starts with '_' and the object
      Mock.new(self.send(m[1..-1]))         # responds to the rest wrap it.
    else                                    # otherwise throw exception or use
      super                                 # object specific method_missing
    end
  end
end

class Mock < BasicObject
  def initialize(me)
    super()
    @me = me
  end
  def method_missing(m, *args, &blk)
    if m[-1] == '_'  # If method ends with '_'
      # If @me isn't nil call m without final '_' and return its result.
      # If @me is nil then return `nil`.
      @me.send(m[0...-1], *args, &blk) if @me 
    else 
      @me = @me.send(m, *args, &blk) if @me # Otherwise call method on `@me` and
      self                                  # store result then return mock.
    end
  end
end

To explain what's going on: when you call an underscored method you trigger mock mode, the result of _meth is wrapped automatically in a Mock object. Anytime you call a method on that mock it checks whether its not holding a nil and then forwards your method to that object (here stored in the @me variable). The mock then replaces the original object with the result of your function call. When you call meth_ it ends mock mode and returns the actual return value of meth.

This allows for an api like this (I used underscores, but you could use really anything):

fizz._buzz.foo.bum.yum.bar_

Brutal monkey-patching approach

This is really quite nasty, but it allows for an elegant API and doesn't necessarily screw up error reporting in your whole app:

class NilClass
  attr_accessor :complain
  def method_missing(*args)
    if @complain
      super
    else
      self
    end
  end
end
nil.complain = true

Use like this:

nil.complain = false
fizz.buzz.foo.bar
nil.complain = true
like image 197
Jakub Hampl Avatar answered Sep 22 '22 20:09

Jakub Hampl


As far as I'm aware there's no really easy way to do this. Some work has been done in the Ruby community that implements the functionality you're talking about; you may want to take a look at:

  • The andand gem
  • Rails's try method

The andand gem is used like this:

require 'andand'
...
fizz.buzz.andand.foo.andand.bar
like image 20
Michelle Tilley Avatar answered Sep 20 '22 20:09

Michelle Tilley