Silent Nils
Nil, Null, None, and Noel, Born is the King of Code Smell
This is a terrible idea.
I was challenged to make nil stop being so whiny in Ruby, as if all . operators were &. operators.
I mean, that’s really quite easy:
class NilClass
def method_missing(*args)
self
end
end
nil.something.another_thing
# => nil
Don’t do that.
But what if we did that?
Ok, let’s not do exactly that but what can we do that’s similar? Obviously we don’t want to actually change nil for everyone, but what if we could do it just on demand? That is, without breaking the rest of Ruby, what if we could declare a specific object as one for which we currently do not care about further nils?
nil is a singleton, so we actually can’t modify it unless doing so globally. But we can roll our own. Let’s start by establishing a pattern in a new object called “nothing”.
require 'singleton'
class NothingClass
include Singleton
def inspect
"nothing"
end
def method_missing(*args)
self
end
end
# Give all objects the ability to know if they are the One True Nothing
module Kernel
def nothing?
self.eql?(NothingClass.instance)
end
end
nothing = NothingClass.instance
# => nothing
nothing.nothing?
# => true
nothing.thing.another_thing
# => nothing
"hello".nothing?
# => false
We’ve defined a method nothing? on Kernel that operates similarly to nil? (also defined on Kernel) so we can check for nothingness. But how do we get there?
Remember, our goal is to mimic safe-navigation (&.) automatically. Our first attempt is a method that you can call when you’re in danger of being nil.
module Kernel
def nothing?
self.eql?(NothingClass.instance)
end
def nothingify
nil? ? NothingClass.instance : self
end
end
We’re going to use the hashie gem because it gives us a rich set of nested method calls that we might use to probe for data (similar to using ActiveRecord model associations and attributes)
require 'hashie'
data = Hashie::Mash.new({x: "hello", y: "world"})
data.x.nothingify.upcase.gsub(/[eE]/,'3')
# => "H3LLO"
data.a.nothingify.upcase.gsub(/[eE]/,'3')
# => nothing
Now if we have an object that might be nil, we can call nothingify on it to be safe from any immediate danger of NoMethodErrors on nil. But as little more than a glorified try, if our nil comes anywhere further down the chain, we’re still boned:
require 'hashie'
response = Hashie::Mash.new({data: {x: "hello", y: "world"}})
response.nothingify.data.x
# => "hello"
response.nothingify.oops.x
# NoMethodError: undefined method `x' for nil:NilClass
response.nothingify.oops.nothingify.x
# => nothing
The error occurs because response.nothingify.oops is still nil, since nothingify simply returned self. We can’t call x on nil.
What we really want is for a single call to nothingify to perpetually call nothingify on any downstream result.
Can we do that?
Deep Nothing
module DeepNothing
CANT_TOUCH_THIS = [
:nothingify,
:deep_nothing,
:nil?,
:methods,
:define_singleton_method,
:convert_key,
]
def deep_nothing
methods.each do |method_name|
next if CANT_TOUCH_THIS.include?(method_name)
define_singleton_method(method_name) do |*args, &blk|
super(*args, &blk).nothingify
end
end
return self
end
end
Object.include(DeepNothing)
module Kernel
def nothing?
self.eql?(NothingClass.instance)
end
def nothingify
nil? ? NothingClass.instance : deep_nothing
end
end
require 'hashie'
response = Hashie::Mash.new({data: {x: "hello", y: "world"}})
response.nothingify
# => #<Hashie::Mash data=#<Hashie::Mash x="hello" y="world">>
response.nothingify.data.x
# => "hello"
response.nothingify.oops.x
# => nothing
Success! (Still a terrible idea.)
Ok, a brief explanation. The method deep_nothing nothing-safes our object by redefining (almost) all of its methods with a wrapper that nothingifys the return value. So a nil return gets turned into nothing, and any other return value get re-nothingify‘d.
We only wrap almost all of the moethods because
- We can’t wrap
nothingifyitself (or any methods called therein, namely,nil?anddeep_nothing) otherwise we’ll get in an infinite loop callingnothingifyrepeatedly - We can’t redefine methods that we’re trying to use inside our method redefinition:
methodsanddefine_singleton_method.
Everything else gets wrapped. (Except for convert_key, which causes trouble putsing a Hashie::Mash for some reason that I cannot figure out).
Now we can nothingify an object once and thereafter be assured to not be bothered by that pesky, no good NoMethodError thing.
Back to the Singleton
One edge-case issue with this approach is singleton methods don’t work because they don’t have a super. Normally, super in this context refers to the original instance method defined by the class. For singleton methods, this doesn’t exist.
obj = Object.new
# define a stingleton method with no relation to the class
def obj.hello
"world"
end
obj.hello
# => "world"
obj.nothingify.hello
# NoMethodError: super: no superclass method `hello' for #<Object:0x007fb472243280>
We can fix that by using method binding instead of super:
module DeepNothing
CANT_TOUCH_THIS = [
:deep_nothing,
:nil?,
:methods,
:method,
:nothingify,
:define_singleton_method,
:convert_key,
]
def deep_nothing
methods.each do |method_name|
next if CANT_TOUCH_THIS.include?(method_name)
saved_method = method(method_name).unbind
define_singleton_method(method_name) do |*args, &blk|
saved_method.bind(self).call(*args, &blk).nothingify
end
end
return self
end
end
Here, when we redefine a method, we first save a reference to the original method. We then call that original method inside our redefined method. No super, just the original result.
obj = Object.new
def obj.hello
"world"
end
obj.hello
# => "world"
obj.nothingify.hello
# => "world"
And then there’s truthiness.
Nothing is true
One critical missing feature of our nothing object is that it evaluates to true in a boolean context.
print "Something" if nothing
# Something
Unfortunately there just isn’t a way around this (prove me wrong in the comments, I beg you!). Ruby, for all its flexibility, doesn’t let you do some things, and defining truthiness is one of them. Perhaps, in this day and age, we can take some comfort in that.