Wrapped

This gem is a tool you can use while developing your API to help consumers of your code to find bugs earlier and faster. It works like this: any time you write a method that could produce nil, you instead write a method that produces a wrapped value.

Warning: Experimental

This library is in an early, experimental phase and may change suddenly and drastically. Backwards-compatibility should not be expected between releases, and the project may be cancelled at any time.

Example

Here's an example along with how it can help with errors:

Say you have a collection of users along with a method for accessing the first user:

class UserCollection
  def initialize(users)
    @users = users
  end

  def first_user
    @users.first
  end
end

Now your friend uses your awesome UserCollection code:

class FriendGroups
  def initialize(user_collections)
    @user_collections = user_collections
  end

  def first_names
    @user_collections.map do |user_collection|
      user_collection.first_user.first_name
    end
  end
end

And then she tries it:

FriendGroups.new([UserCollection.new([])]).first_names

... and it promptly blows up:

NoMethodError: undefined method `first_name' for nil:NilClass
        from (irb):52:in `first_names'
        from (irb):51:in `map'
        from (irb):51:in `first_names'
        from (irb):57

But that's odd; UserCollection definitely has a #first_names method, and we definitely passed a UserCollection, and ... oooh, we passed no users, and so we got nil.

Right.

Instead what you want to do is wrap it. Wrap that nil. Make the user know that they have to consider the result.

class UserCollection
  def initialize(users)
    @users = users
  end

  def first_user
    @users.first.wrapped
  end
end

Now in your documentation you explain that it produces a wrapped value. And people who skip documentation and instead read source code will see that it is wrapped.

So they unwrap it, because they must. They can't even get a happy path without unwrapping it.

class FriendGroups
  def initialize(user_collections)
    @user_collections = user_collections
  end

  def first_names
    @user_collections.map do |user_collection|
      user_collection.first_user.unwrap_or('') {|user| user.first_name }
    end
  end
end

Cool Stuff

A wrapped value mixes in Enumerable. The functional world would say "that's a functor!". They're close enough.

This means that you can map, inject, to_a, any?, and so on over your wrapped value. By wrapping it you've just made it more powerful!

For example:

irb(main):054:0> 1.wrapped.inject(0) {|_, n| n+1}
=> 2
irb(main):055:0> nil.wrapped.inject(0) {|_, n| n+1}
=> 0

And then we have flat_map, which you can use to produce another wrapped object:

irb> 1.wrapped.flat_map {|n| (n + 1).wrapped}.flat_map {|n| (n*2).wrapped}.unwrap
=> 4

Those same people who will exclaim things about functors will, at this point, get giddy about monads. I mean, they're right, but they can relax. It's just a monad.

Those people ("what do you mean, 'those people'?!") may prefer the fmap method:

irb> 1.wrapped.fmap {|n| n+1}.unwrap_or(0) {|n| n+4}
=> 6

Other Methods

Then we added some convenience methods to all of this. Here's a tour:

irb> require 'wrapped'
=> true
irb> 1.wrapped.unwrap_or(-1)
=> 1
irb> nil.wrapped.unwrap_or(-1)
=> -1
irb> 1.wrapped.present {|n| p n }.blank { puts "nothing!" }
1
=> #<Present:0x7fc570aed0e8 @value=1>
irb> nil.wrapped.present {|n| p n }.blank { puts "nothing!" }
nothing!
=> #<Blank:0x7fc570ae21c0>
irb> 1.wrapped.unwrap
=> 1
irb> nil.wrapped.unwrap
IndexError: Blank has no value
    from /home/mike/wrapped/lib/wrapped/types.rb:43:in `unwrap'
    from (irb):7
irb> 1.wrapped.present?
=> true
irb> nil.wrapped.present?
=> false
irb> nil.wrapped.blank?
=> true
irb> 1.wrapped.blank?
=> false
irb> 1.wrapped.unwrap_or(0) {|n| n * 100}
=> 100
irb> nil.wrapped.unwrap_or(0) {|n| n * 100}
=> 0

Inspiration

Inspired by a conversation about a post on the thoughtbot blog titled If you gaze into nil, nil gazes also into you.

Most ideas are from Haskell and Scala. This is not new: look into the maybe functor or the option class for more.

Copyright 2011 Mike Burns. Copyright 2014 thoughtbot.