For the past six months or so I've been working an NES emulator in Rust. As you might expect, I've learned a lot about rust, and even more about NES internals. But the experience has also changed the way I look at Ruby.

Specifically, it's made me more than a little paranoid about methods that return nil.

If You Don't Stand for Something, You'll Fall for Anything

What does nil mean in Ruby? Almost anything. When a method returns nil, it could mean:

  • The method has no return value
  • There's usually a return value but not this time
  • It returns a value from the database, which is NULL
  • Something unexpected happened

This makes code hard to read and is the main cause of the most common Ruby exception in Ruby: NoMethodError. As part-owner of an exception monitoring service, NoMethodError is putting my kid through school.

Look at the following code. It returns nil most of the time because if statements evaluate to nil when the conditional doesn't match and there's no else.

def color_name(rgb)
  if rgb == 0x000000
    "black"
  end
end

color_name("#FFFFFF").titleize
=> NoMethodError: undefined method `titleize' for nil:NilClass

If you're an experienced Ruby developer, you know this rabbit hole goes much deeper. Sometimes these different meanings of nil overlap in strange ways, making it impossible to know whether — for example — a value in a database is NULL, or there's no value in the database at all.

A Better Way

In Rust there's no such thing as nil. Instead, when we want to signify that a function sometimes returns a value and sometimes returns "nothing" we use an Option.

Options are a type that either contains some specific value, or contains no value. Here's what they look like in code:

Option::Some(42); // Wraps the number 42 in an option
Option::None;     // Indicates "no result"

This is already looking better than our ad-hoc nil usage, but it gets even better. The Rust compiler forces you to consider the None case. You can't accidentally ignore it.

match my_option {
  Some(x) => do_something_with_x(x),
  // If you remove the `None` match below, this code
  // won't compile.
  None => do_the_default_thing()  
}

So we could write our color naming example in rust like so:

fn color_name(rgb: u32) -> Option<String> {
    if rgb == 0x000000 {
      Some("black".to_owned())
    } else {
      None
    }
}

Now we're forced to handle both the Some and None conditions:

let name = color_name(0xFFFFFF);

let name = match color_name(0xFFFFFF) {
  Some(value) => value,
  None => "unknown".to_owned(),
}

Sure this is a little verbose and weird looking, but it makes it impossible to ignore the case when a function doesn't return a useful value. That means code that's easier to understand and maintain.

Implementing Option in Ruby

What Ruby lacks in rigor it makes up for in flexibility. I thought it'd be interesting to try to implement something like Option in Ruby.

We can't create compile-time errors in Ruby, since it's an interpreted language. But we can cause incorrect code to always raise an exception, instead of only raising an exception when you hit an edge case.

First, let's create two classes. Some holds a read-only value. None is empty. They're as simple as they seem.

  class Some
    attr_reader :value
    def initialize(value)
      @value = value
    end
  end

  class None
  end

Next, we'll create our Option class which holds either Some or None and only lets us access them when we provide handlers for both.

class Option
  def initialize(value)
    @value = value
  end

  def self.some(value)
    self.new(Some.new(value))
  end

  def self.none()
    self.new(None.new)
  end

  def match(some_lambda, none_lambda)
    if @value.is_a?(Some)
      some_lambda.call(@value.value)
    elsif @value.is_a?(None)
      none_lambda.call()
    else
      raise "Option value must be either Some or None"
    end
 end
end

Finally, we can rewrite our color example to use the new Option class:

def color_name(rgb)
  if rgb == 0x000000
    Option.some("black")
  else
    Option.none()
  end
end

puts color_name(0x000000).match(
  -> value { value },
  -> { "no match" })

# Prints "black"

Conclusion

I've yet to try this technique in a real project. I think it could definitely prevent a lot of the NoMethodErrors that always slip into production. It is a bit cumbersome looking, and not very Rubyish but I imagine that with some refinement a more pleasant syntax would emerge.