Iffy_code


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:

  1. .do and .or both return self now
  2. 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.

See also