Ah, Null. Infamously referred to as the billion-dollar mistake.
As software engineers, we likely spend a quarter of our careers managing Null. Checking for it. Returning it.
Trying to figure out WTF returned Null somewhere down there that caused an invalid de-reference here…
No getting around it. Development in languages with Null can be Iffy.
I’m a Ruby dev in my day job and after writing some variant of
if !foo.nil?
do_the_thing(foo)
else
dont
end
for probably the hundred-millionth time in my career, I had a thought.
“Wouldn’t it be nice,” thought I, “if I could just write foo.do {|afoo| do_the_thing(afoo)}.or { dont }
”
And then I thought, “That seems… doable.” All we need is a class with two methods and a single value.
If the value is nil
(Ruby for Null), then .do
does nothing. On the other hand, if the value is nil
, then .or
should evaluate its block. And vice-versa for non-nil values.
Let’s write up a little wrapper class:
class Iffy
def initialize(val)
# capture a value to wrap
@value = val # @-prefixed vars are instance variables.
end
def do(&block)
# Ruby allows us to accept closures as method arguments.
# The & prefix indicates that this method can be called
# like `foo.do { closure_code }`. Just a bit of syntax sugar.
if !@value.nil?
# Since our value is not nil, call the block and pass it in!
block.call(@value)
end
end
def or(&block)
if @value.nil?
# We don't pass @value to the block - it's nil!
block.call
end
end
end
Let’s use it!
def iffy_code
val = "not nil" # Just pretend there was a function call here, k?
Iffy.new(val)
.do { |real_val| puts real_val }
.or { puts "Nope - nil!" }
end
> iffy_code
not nil
(irb):29:in `iffy_code': undefined method `or' for nil:NilClass (NoMethodError)
from (irb):57:in `<main>'
from /usr/lib/ruby/gems/3.1.0/gems/irb-1.4.1/exe/irb:11:in `<top (required)>'
from /usr/bin/irb:25:in `load'
from /usr/bin/irb:25:in `<main>'
Wait. What happened there?
Our .do
block executed just fine, but then we got a nil reference for .or
?
Oh right - method chaining.
When we called .do
it was on an instance of Iffy
. But .or
got invoked on
the return value of the .do
block! nil
in this case since that’s what puts
returns! And of course, nil
doesn’t know about .or
.
Let’s make sure we return our Iffy self so that we are always calling .do
and
.or
on our Iffy
object.
# Iffy class
def do(&block)
block.call(@value) if !@value.nil?
self
end
def or(&block)
block.call if @value.nil?
self
end
Two changes here:
.do
and.or
both return self now- We refactored the if syntax from the multi-line blocks to one line statements. It’s a Ruby thing.
With that change, we try again:
> iffy_code
not nil
=> #<Iffy:0x00007fcfb9a30490 @value="not nil">
Much better! Let’s try it with nil:
Iffy.new(nil).do {|v| puts "not nil"}.or{ puts "nil!"}
nil!
=> #<Iffy:0x00007fcfb9a0a678 @value=nil>
It’s kind of a pain wrapping everything in Iffy
though. What if we just made … everything Iffy?
NOTE: This is a bad (but cool) idea. If you try to do this at Work Inc. don’t blame me if you get fired.
You’ve been warned. This way madness lay.
First, we turn Iffy
into a module and replace @value
with self
module Iffy
def do(&block)
block.call(self) if !self.nil?
self
end
def or(&block)
block.call if self.nil?
self
end
end
And then we extend… everything.
Object.include(Iffy)
Ruby lets you do all kinds of wild things. Like the Object
class being open for extension.
We can - at runtime - include our module which will define our methods on everything that
inherits from Object
… which is everything!
(This is Ruby’s famed monkey patching feature! Very powerful. Very dangerous.)
> 1.do {|n| puts "I am numero #{n}!" }.or { raise "won't get here" }
I am numero 1!
=> 1
> nil.do {|??| raise "cannot do it!"}.or{ puts "null!"}
null!
=> nil
Whoa. Now we can invoke .do
and .or
on any value anywhere in our program!
def update_widget_color(type, color)
find_widget(type: type).do { |w|
w.update(color: color)
.save!
}.or {
create_widget(type: type, color: color)
}
end
We can take this one step further. nil
is actually a singleton instance of a class named NilClass
.
…Which we can also monkey patch. :D Let’s split our module in two:
module IffyPresent
def do(&block)
self.tap { block.call(self) }
end
def or(&_block)
# no-op
self
end
end
module IffyNil
def do(&_block)
# no-op
self
end
def or(&block)
block.call
nil
end
end
# And then include them on our types
Object.include(IffyPresent) # Everybody has value
NilClass.include(IffyNil) # Except for nil - override .do and .or for them
Now the desired behavior is encoded directly in our type system!
nil
instances will pass through .do
and invoke .or
and non-nil values will do the opposite - all without branching logic in our
code base.
Of course, you probably shouldn’t do this…. but it’s pretty cool that you can.