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.
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.
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.
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.
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.
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
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_
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
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:
try
method
The andand gem is used like this:
require 'andand'
...
fizz.buzz.andand.foo.andand.bar
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With