Skip to content

rsslldnphy/optional

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

99 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

optional

option types to make nils a thing of the past

Build Status Code Climate

Tony Hoare, inventor of the null reference, calls it his "billion-dollar mistake". You will be no stranger to the ubiquitous NoMethodError: undefined method 'foo' for nil:NilClass. But it doesn't have to be this way.

There are, crucially, two distinct types of values (or rather lack of values) that are usually, in Ruby, represented as nil. There are values that should always be present - and here the fact that they are nil actually indicates that an error has occurred somewhere else in your program - and those values that may or may not be set - where each case is valid. For example, a person may or may not be wearing a hat.

Using nil to represent these optional values leads to lots of ugly defensive coding - or worse, if you forget the ugly defensive coding in even a single place, to nils leaking out into the rest of your program, causing it to blow up at some later point, from where it might be hard to track back to the original cause of the problem.

Options, a construct found in many functional languages, provide an alternative. They consist of, at the most basic level, a container that either contains a value (Some value) or does not (None). Because its a container, you can't access the value directly, so client code is forced to deal with both the case that it's there and that it isn't. It's a simple concept but leads to some surprisingly powerful uses.

Creating Options

Here's how you create an option that has a value (a Some), using some syntactic sugar provided by the [] method:

Some[4]
Some[Cat.new("Tabitha")]
Some["Jelly"]

Like nil, there's only one instance of None, so you don't need to create it. Here's how you use it:

None

Getting the value

The simplest way to get the value of an Option is using the value method:

Some[5].value # => 5
None.value    # => raises ValueOfNoneError

Why is it a good thing that calling value on None raises an error? Because you should only use value if you're sure that you should have a value at this point - if not having a value is an error. This means the code will blow up at the earliest possible opportunity, making it easier to track down the root cause of the problem.

How do I know if I have a value?

There are methods named as you'd expect to test for this:

Some[5].some? # => true
Some[5].none? # => false

None.some? # => false
None.none? # => true

You can also pass a class or module constant to some?:

Some["Perpugilliam Brown"].some? String # => true
Some["Perpugilliam Brown"].some? Fixnum # => false

This can come in handy when writing rspec tests:

it { should be_none }
it { should be_some Fixnum }

But you shouldn't usually need to use these some? or none? methods. They basically add up to exactly the same ugly defensive code we're trying to avoid with nils. Luckily Options provide plenty of other, more elegant ways to access their values.

Providing a default

If not having a value is a valid case, you might want to provide a default in its place. You can do this using the value_or method:

Some[5].value_or 0 # => 5
None.value_or 0    # => 0

You can also pass a block to value_or in case you don't want the default to be evaluated unless it is used:

Some[5].value_or { puts "This won't be printed." ; 0 } # => 5
None.value_or { puts "This will be printed!" ; 0 }     # => 0

Option is enumerable!

Option supports all the same methods that enumerable supports - although it will return Options rather than Arrays in most cases. This leads to some pretty cool stuff.

Here's each:

Some[5].each do |value|
  puts "The value is #{value}!"
end
# => prints "The value is 5!"

None.each do |value|
  puts "The value is #{value}!"
end
# => doesn't print anything (because there is no value)

Or you can also use for:

for value in Some[5]
  puts "The value is #{value}!"
end
# => prints "The value is 5!"

for value in None
  puts "The value is #{value}!"
end
# => doesn't print anything

And here's map (or collect):

Some["caterpillar!"].map(&:upcase) # => Some["CATERPILLAR!"]
None.map(&:upcase) # => None

Options being enumerable comes in handy if you're using rails, too. Let's say you have a person model that has an Option[Hat], and you want to render a Hat partial only if the person has one. You can simply use the collection key when rendering the partial - you don't even need an if:

<%= render partial: 'hat', collection: @person.hat %>

You can also use select and reject to assert things about the value:

Some[5].select(&:odd?) # => Some[5]
Some[5].reject(&:odd?) # => None

And one last useful example, flatten:

Some[Some[5], None, Some[7]].flatten # => Some[5,7]

[Some[6], None, Some[999]].flatten   # => [6,999]

Pattern-matching

You'll find Options in many functional languages such as ML, Haskell (as the Maybe monad), Scala and F#. And in most cases, they will provide a way to access the values (not specific to Options, but a more general part of the language), called pattern-matching. We don't have pattern-matching in Ruby, but Optional provides a basic version for use with Options. Here's how to use it:

option.match do |m|
  m.some { |value| puts "The value is #{value}" }
  m.none { puts "No value here" }
end

As you'd expect, the some branch is executed if there is a value, the none branch if there isn't.

You can also add cases that assert based on the content of the option:

option.match do |m|
  m.some(:fedora) { puts "This will be printed only if I'm passed a Some[:fedora]" }
  m.some(:trilby) { puts "This will be printed only if I'm passed a Some[:trilby]" }
  m.none          { puts "This is printed if I'm passed a None" }
end

The first case that matches is the one the match clause evaluates to.

You can also use lambdas as part of the match clauses like this:

option.match do |m|
  m.some ->(x){ x.length > 2 }  { puts "Printed if value's length is > 2" }
  m.some ->(x){ x.is_a? Array } { puts "Printed if value is an array (with lengt <= 2)" }
  m.none                        { puts "This is printed if I'm passed a None" }
end

Extra fun and goodness

What's been described so far is what you'd usually expect from Options in other languages. Optional, however, provides a few extra bits and pieces you may want to have a play with.

Logical operators (sort of logical, anyway)

Got two optional values and want to do something only if they both have values? Use &:

Some[5] & Some[6]  # => Some[5,6]
Some[5] & None     # => None
None    & Some[5]  # => None
None    & None     # => None

Got two optional values, either of which might be None, and want to do something with one of them, doesn't matter which? Use |:

Some[5] | Some[6]  # => Some[5]
Some[5] | None     # => Some[5]
None    | Some[6]  # => Some[6]
None    | None     # => None

NB. Technically, an Option should only have up to one value, but to allow the & operator and similar things Optional sort of cheats by treating 'multiple values' as a single value of type Array.

Mapping through multiple functions

You might find yourself needing to map an optional value through a number of functions. There's a handy way to do this with Options:

p = Person.create(name: "Russell!")

Some[p].map_through(:name, :upcase) # => Some["RUSSELL!"]
None.map_through(:name, :upcase) # => None

Juxtaposing the result of multiple functions

This one is nicked from Clojure (there might be other languages that have it, I don't know). Call a list of functions on an option value, returning a Some of their results:

p = Person.create(name: "Russell!", age: 29, hat: :fedora)

Some[p].juxt(:name, :age, :class) # => Some["Russell", 29, Person]
None.juxt(:name, :age, :class) # => None

If you're feeling adventurous you could always monkey patch this onto Object so it's available for everything. Might come in useful.

That's about it

I hope you find Options useful. Let me know if you do: I'm @rsslldnphy on that Twitter. I'm very enthusiastic about optional types and functional programming techniques in general, so if you have a question or want advice on using options in your project I'll be happy to answer!

If you'd like to contribute, create a pull request for the feature or fix you have in mind. That way we can discuss whether it's a good fit and how best to approach it before you start on the code. Contributions very welcome!

About

Optional values with pattern matching

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages