Browse files

The wrapped gem: the maybe functor for Ruby.

  • Loading branch information...
0 parents commit 3e37462111ff7c6d394d6982728d95e0d960d7eb @mike-burns mike-burns committed Jul 28, 2011
Showing with 425 additions and 0 deletions.
  1. +5 −0 .gitignore
  2. +4 −0 Gemfile
  3. +30 −0 LICENSE
  4. +132 −0 README.md
  5. +6 −0 Rakefile
  6. +2 −0 lib/wrapped.rb
  7. +13 −0 lib/wrapped/injection.rb
  8. +70 −0 lib/wrapped/types.rb
  9. +3 −0 lib/wrapped/version.rb
  10. +1 −0 spec/spec_helper.rb
  11. +138 −0 spec/wrapped_spec.rb
  12. +21 −0 wrapped.gemspec
5 .gitignore
@@ -0,0 +1,5 @@
+*.gem
+.bundle
+Gemfile.lock
+pkg/*
+*swp
4 Gemfile
@@ -0,0 +1,4 @@
+source "http://rubygems.org"
+
+# Specify your gem's dependencies in wrapped.gemspec
+gemspec
30 LICENSE
@@ -0,0 +1,30 @@
+Copyright (c)2011, Mike Burns
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+
+ * Neither the name of Mike Burns nor the names of other
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
132 README.md
@@ -0,0 +1,132 @@
+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.
+
+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 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.inject('') do |default,user|
+ user.first_name
+ end
+ end
+ end
+ end
+
+Cool Stuff
+----------
+
+In the example above I made use of the fact that a wrapped value mixes in Enumerable. The functional world would say "that's a functor!". That's right it is.
+
+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!
+
+Other Methods
+-------------
+
+Then we added some convenience methods to all of this. Here's a tour:
+
+ irb(main):001:0> require 'wrapped'
+ => true
+ irb(main):002:0> 1.wrapped.unwrap_or(-1)
+ => 1
+ irb(main):003:0> nil.wrapped.unwrap_or(-1)
+ => -1
+ irb(main):004:0> 1.wrapped.present {|n| p n }.blank { puts "nothing!" }
+ 1
+ => #<Present:0x7fc570aed0e8 @value=1>
+ irb(main):005:0> nil.wrapped.present {|n| p n }.blank { puts "nothing!" }
+ nothing!
+ => #<Blank:0x7fc570ae21c0>
+ irb(main):006:0> 1.wrapped.unwrap
+ => 1
+ irb(main):007:0> nil.wrapped.unwrap
+ IndexError: Blank has no value
+ from /home/mike/wrapped/lib/wrapped/types.rb:43:in `unwrap'
+ from (irb):7
+ irb(main):008:0> 1.wrapped.present?
+ => true
+ irb(main):009:0> nil.wrapped.present?
+ => false
+ irb(main):010:0> nil.wrapped.blank?
+ => true
+ irb(main):011:0> 1.wrapped.blank?
+ => false
+
+Inspiration
+-----------
+
+Inspired by a conversation about a post on the thoughtbot blog titled "If you gaze into nil, nil gazes also into you":
+ http://robots.thoughtbot.com/post/8181879506/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 monad or the option class for more.
+
+Copyright
+---------
+Copyright 2011 Mike Burns
6 Rakefile
@@ -0,0 +1,6 @@
+require 'bundler/gem_tasks'
+require 'rspec/core/rake_task'
+
+RSpec::Core::RakeTask.new(:spec)
+
+task :default => :spec
2 lib/wrapped.rb
@@ -0,0 +1,2 @@
+require 'wrapped/version'
+require 'wrapped/injection'
13 lib/wrapped/injection.rb
@@ -0,0 +1,13 @@
+require 'wrapped/types'
+
+class Object
+ def wrapped
+ Present.new(self)
+ end
+end
+
+class NilClass
+ def wrapped
+ Blank.new
+ end
+end
70 lib/wrapped/types.rb
@@ -0,0 +1,70 @@
+class Present
+ include Enumerable
+
+ def initialize(value)
+ @value = value
+ end
+
+ def unwrap_or(_)
+ unwrap
+ end
+
+ def present(&block)
+ block.call(unwrap)
+ self
+ end
+
+ def blank(&ignored)
+ self
+ end
+
+ def each(&block)
+ block.call(unwrap) unless block.nil?
+ [unwrap]
+ end
+
+ def unwrap
+ @value
+ end
+
+ def present?
+ true
+ end
+
+ def blank?
+ false
+ end
+end
+
+class Blank
+ include Enumerable
+
+ def unwrap
+ raise IndexError.new("Blank has no value")
+ end
+
+ def unwrap_or(default)
+ default
+ end
+
+ def present(&ignored)
+ self
+ end
+
+ def blank(&block)
+ block.call
+ self
+ end
+
+ def each(&ignored)
+ []
+ end
+
+ def present?
+ false
+ end
+
+ def blank?
+ true
+ end
+end
3 lib/wrapped/version.rb
@@ -0,0 +1,3 @@
+module Wrapped
+ VERSION = "0.0.1"
+end
1 spec/spec_helper.rb
@@ -0,0 +1 @@
+require 'wrapped'
138 spec/wrapped_spec.rb
@@ -0,0 +1,138 @@
+require 'spec_helper'
+
+describe Wrapped, 'conversion' do
+ let(:value) { 1 }
+ let(:just) { 1.wrapped }
+ let(:nothing) { nil.wrapped }
+
+ it "converts the value to a Present" do
+ just.should be_instance_of(Present)
+ end
+
+ it "converts the nil to a Blank" do
+ nothing.should be_instance_of(Blank)
+ end
+end
+
+describe Wrapped, 'accessing' do
+ let(:value) { 1 }
+ let(:just) { 1.wrapped }
+ let(:nothing) { nil.wrapped }
+
+ it 'produces the value of the wrapped object' do
+ just.unwrap.should == value
+ end
+
+ it 'raises an exception when called on the wrapped nil' do
+ expect { nothing.unwrap }.to raise_error(IndexError)
+ end
+end
+
+describe Wrapped, 'callbacks' do
+ let(:value) { 1 }
+ let(:just) { 1.wrapped }
+ let(:nothing) { nil.wrapped }
+
+ it 'calls the proper callback for a wrapped value' do
+ result = false
+ just.present {|v| result = v}
+ result.should be_true
+ end
+
+ it 'calls the proper callback for a wrapped nil' do
+ result = false
+ nothing.blank {result = true}
+ result.should be_true
+ end
+
+ it 'ignores the other callback for a wrapped value' do
+ result = true
+ just.blank { result = false }
+ result.should be_true
+ end
+
+
+ it 'ignores the other callback for a wrapped nil' do
+ result = true
+ nothing.present { result = false }
+ result.should be_true
+ end
+
+ it 'chains for wrapped values' do
+ result = false
+ just.present { result = true }.blank { result = false }
+ result.should be_true
+ end
+
+ it 'chains for wrapped nils' do
+ result = false
+ nothing.present { result = false }.blank { result = true }
+ result.should be_true
+ end
+end
+
+# This behavior is different from Haskell and Scala.
+# It is done this way for consistency with Ruby.
+describe Wrapped, 'enumerable' do
+ let(:value) { 1 }
+ let(:just) { 1.wrapped }
+ let(:nothing) { nil.wrapped }
+
+ it 'acts over the value for #each on a wrapped value' do
+ result = -1
+ just.each {|v| result = v }
+ result.should == value
+ end
+
+ it 'produces a singleton array of the value for a wrapped value on #each' do
+ just.each.should == [value]
+ end
+
+ it 'skips the block for #each on a wrapped nil' do
+ result = -1
+ nothing.each {|v| result = v }
+ result.should == -1
+ end
+
+ it 'produces the empty array for a wrapped nil on #each' do
+ nothing.each.should be_empty
+ end
+
+ it 'maps over the value for a wrapped value' do
+ just.map {|n| n + 1}.should == [value+1]
+ end
+
+ it 'map produces the empty list for a wrapped nil' do
+ nothing.map {|n| n + 1}.should == []
+ end
+end
+
+describe Wrapped, 'queries' do
+ let(:value) { 1 }
+ let(:just) { 1.wrapped }
+ let(:nothing) { nil.wrapped }
+
+ it 'knows whether it is present' do
+ just.should be_present
+ nothing.should_not be_present
+ end
+
+ it 'knows whether it is blank' do
+ just.should_not be_blank
+ nothing.should be_blank
+ end
+end
+
+describe Wrapped, 'unwrap_or' do
+ let(:value) { 1 }
+ let(:just) { 1.wrapped }
+ let(:nothing) { nil.wrapped }
+
+ it 'produces the value for a wrapped value' do
+ just.unwrap_or(-1).should == value
+ end
+
+ it 'produces the default for a wrapped nil' do
+ nothing.unwrap_or(-1).should == -1
+ end
+end
21 wrapped.gemspec
@@ -0,0 +1,21 @@
+# -*- encoding: utf-8 -*-
+$:.push File.expand_path("../lib", __FILE__)
+require "wrapped/version"
+
+Gem::Specification.new do |s|
+ s.name = "wrapped"
+ s.version = Wrapped::VERSION
+ s.authors = ["Mike Burns"]
+ s.email = ["mike@mike-burns.com"]
+ s.homepage = ""
+ s.summary = %q{The maybe functor for Ruby}
+ s.description = %q{The unchecked nil is a dangerous concept leading to NoMethodErrors at runtime. It would be better if you were forced to explictly unwrap potentially nil values. This library provides mechanisms and convenience methods for making this more possible.}
+
+ s.files = `git ls-files`.split("\n")
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
+ s.require_paths = ["lib"]
+
+ s.add_development_dependency('rspec')
+ s.add_development_dependency('rake')
+end

0 comments on commit 3e37462

Please sign in to comment.