wrapped 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ *swp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in wrapped.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,30 @@
1
+ Copyright (c)2011, Mike Burns
2
+
3
+ All rights reserved.
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ * Redistributions of source code must retain the above copyright
9
+ notice, this list of conditions and the following disclaimer.
10
+
11
+ * Redistributions in binary form must reproduce the above
12
+ copyright notice, this list of conditions and the following
13
+ disclaimer in the documentation and/or other materials provided
14
+ with the distribution.
15
+
16
+ * Neither the name of Mike Burns nor the names of other
17
+ contributors may be used to endorse or promote products derived
18
+ from this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24
+ OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.md ADDED
@@ -0,0 +1,169 @@
1
+ Wrapped
2
+ -------
3
+
4
+ This gem is a tool you can use while developing your API to help consumers of
5
+ your code to find bugs earlier and faster. It works like this: any time you
6
+ write a method that could produce nil, you instead write a method that produces
7
+ a wrapped value.
8
+
9
+ Example
10
+ -------
11
+
12
+ Here's an example along with how it can help with errors:
13
+
14
+ Say you have a collection of users along with a method for accessing the first
15
+ user:
16
+
17
+ class UserCollection
18
+ def initialize(users)
19
+ @users = users
20
+ end
21
+
22
+ def first_user
23
+ @users.first
24
+ end
25
+ end
26
+
27
+ Now your friend uses your awesome UserCollection code:
28
+
29
+ class FriendGroups
30
+ def initialize(user_collections)
31
+ @user_collections = user_collections
32
+ end
33
+
34
+ def first_names
35
+ @user_collections.map do |user_collection|
36
+ user_collection.first_user.first_name
37
+ end
38
+ end
39
+ end
40
+
41
+ And then she tries it:
42
+
43
+ FriendGroups.new([UserCollection.new([])]).first_names
44
+
45
+ ... and it promptly blows up:
46
+
47
+ NoMethodError: undefined method `first_name' for nil:NilClass
48
+ from (irb):52:in `first_names'
49
+ from (irb):51:in `map'
50
+ from (irb):51:in `first_names'
51
+ from (irb):57
52
+
53
+ But that's odd; UserCollection definitely has a `#first_names` method, and we
54
+ definitely passed a UserCollection, and ... oooh, we passed no users, and so we
55
+ got `nil`.
56
+
57
+ Right.
58
+
59
+ Instead what you want to do is wrap it. Wrap that nil. Make the user know that
60
+ they have to consider the result.
61
+
62
+ class UserCollection
63
+ def initialize(users)
64
+ @users = users
65
+ end
66
+
67
+ def first_user
68
+ @users.first.wrapped
69
+ end
70
+ end
71
+
72
+ Now in your documentation you explain that it produces a wrapped value. And
73
+ people who skip documentation and instead read source code will see that it is
74
+ wrapped.
75
+
76
+ So they unwrap it, because they must. They can't even get a happy path without
77
+ unwrapping it.
78
+
79
+ class FriendGroups
80
+ def initialize(user_collections)
81
+ @user_collections = user_collections
82
+ end
83
+
84
+ def first_names
85
+ @user_collections.map do |user_collection|
86
+ user_collection.first_user.unwrap_or('') {|user| user.first_name }
87
+ end
88
+ end
89
+ end
90
+
91
+ Cool Stuff
92
+ ----------
93
+
94
+ A wrapped value mixes in Enumerable. The functional world would say "that's a
95
+ functor!". They're close enough.
96
+
97
+ This means that you can `map`, `inject`, `to_a`, `any?`, and so on over your
98
+ wrapped value. By wrapping it you've just made it more powerful!
99
+
100
+ For example:
101
+
102
+ irb(main):054:0> 1.wrapped.inject(0) {|_, n| n+1}
103
+ => 2
104
+ irb(main):055:0> nil.wrapped.inject(0) {|_, n| n+1}
105
+ => 0
106
+
107
+ And then we have `try`, which you can use to produce another wrapped object:
108
+
109
+ irb> 1.wrapped.try {|n| (n + 1).wrapped}.try {|n| (n*2).wrapped}.unwrap
110
+ => 4
111
+
112
+ Those same people who will exclaim things about functors will, at this point,
113
+ get giddy about monads. I mean, they're right, but they can relax. It's just a
114
+ monad.
115
+
116
+ Those people ("what do you mean, 'those people'?!") may prefer the `fmap`
117
+ method:
118
+
119
+ irb> 1.wrapped.fmap {|n| n+1}.unwrap_or(0) {|n| n+4}
120
+ => 6
121
+
122
+ Other Methods
123
+ -------------
124
+
125
+ Then we added some convenience methods to all of this. Here's a tour:
126
+
127
+ irb> require 'wrapped'
128
+ => true
129
+ irb> 1.wrapped.unwrap_or(-1)
130
+ => 1
131
+ irb> nil.wrapped.unwrap_or(-1)
132
+ => -1
133
+ irb> 1.wrapped.present {|n| p n }.blank { puts "nothing!" }
134
+ 1
135
+ => #<Present:0x7fc570aed0e8 @value=1>
136
+ irb> nil.wrapped.present {|n| p n }.blank { puts "nothing!" }
137
+ nothing!
138
+ => #<Blank:0x7fc570ae21c0>
139
+ irb> 1.wrapped.unwrap
140
+ => 1
141
+ irb> nil.wrapped.unwrap
142
+ IndexError: Blank has no value
143
+ from /home/mike/wrapped/lib/wrapped/types.rb:43:in `unwrap'
144
+ from (irb):7
145
+ irb> 1.wrapped.present?
146
+ => true
147
+ irb> nil.wrapped.present?
148
+ => false
149
+ irb> nil.wrapped.blank?
150
+ => true
151
+ irb> 1.wrapped.blank?
152
+ => false
153
+ irb> 1.wrapped.unwrap_or(0) {|n| n * 100}
154
+ => 100
155
+ irb> nil.wrapped.unwrap_or(0) {|n| n * 100}
156
+ => 0
157
+
158
+ Inspiration
159
+ -----------
160
+
161
+ Inspired by a conversation about a post on the thoughtbot blog titled [If you
162
+ gaze into nil, nil gazes also into you](http://robots.thoughtbot.com/post/8181879506/if-you-gaze-into-nil-nil-gazes-also-into-you).
163
+
164
+ Most ideas are from Haskell and Scala. This is not new: look into the maybe
165
+ functor or the option class for more.
166
+
167
+ Copyright
168
+ ---------
169
+ Copyright 2011 Mike Burns
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/lib/wrapped.rb ADDED
@@ -0,0 +1,2 @@
1
+ require 'wrapped/version'
2
+ require 'wrapped/injection'
@@ -0,0 +1,66 @@
1
+ # The class represents a lack of something.
2
+ class Blank
3
+ include Enumerable
4
+
5
+ # It is an error (specifically, an IndexError) to use this method.
6
+ def unwrap
7
+ raise IndexError.new("Blank has no value")
8
+ end
9
+
10
+ # Produce the value that is passed in.
11
+ #
12
+ # > w.unwrap_or(0)
13
+ def unwrap_or(default)
14
+ default
15
+ end
16
+
17
+ # Does nothing, returning itself. This is chainable. See blank for its
18
+ # companion.
19
+ #
20
+ # w.present.blank { puts "Missing" }
21
+ def present
22
+ self
23
+ end
24
+
25
+ # Call the block then return itself. This is chainable. See present for its
26
+ # companion.
27
+ #
28
+ # w.blank { puts "I got nothing" }.present {|n| puts "got #{n}" }
29
+ def blank(&block)
30
+ block.call
31
+ self
32
+ end
33
+
34
+ # Produce the empty list.
35
+ #
36
+ # This class mixes in the Enumerable module, which relies on this.
37
+ #
38
+ # > w.each {|n| puts n }
39
+ def each
40
+ []
41
+ end
42
+
43
+ # False; this is not an instance of a wrapped value.
44
+ def present?
45
+ false
46
+ end
47
+
48
+ # True; this is an instance of nothing.
49
+ def blank?
50
+ true
51
+ end
52
+
53
+ # Do nothing, returning itself.
54
+ #
55
+ # > w.try {|n| n+1 }
56
+ def try
57
+ self
58
+ end
59
+
60
+ # Do nothing, returning itself.
61
+ #
62
+ # > w.fmap {|n| n+1 }
63
+ def fmap
64
+ self
65
+ end
66
+ end
@@ -0,0 +1,22 @@
1
+ require 'wrapped/present'
2
+ require 'wrapped/blank'
3
+
4
+ class Object
5
+ # Wrap the object, forcing the user to be aware of the potential for a nil
6
+ # value by unwrapping it.
7
+ #
8
+ # See the Present class for details on how to unwrap it.
9
+ def wrapped
10
+ Present.new(self)
11
+ end
12
+ end
13
+
14
+ class NilClass
15
+ # Wrap the nil, which is exactly the whole point. At this point the user must
16
+ # explictly deal with the nil, aware that it exists.
17
+ #
18
+ # See the Blank class and the README for details on how to work with this.
19
+ def wrapped
20
+ Blank.new
21
+ end
22
+ end
@@ -0,0 +1,89 @@
1
+ # A class that represents a wrapped value. This class holds something that is
2
+ # not nil.
3
+ class Present
4
+ include Enumerable
5
+
6
+ # Use Object#wrapped and NilClass#wrapped instead.
7
+ def initialize(value) # :nodoc:
8
+ @value = value
9
+ end
10
+
11
+ # Produce the value, unwrapped.
12
+ #
13
+ # If a block is given apply the block to the value and produce that.
14
+ #
15
+ # > w.unwrap_or(0)
16
+ # > w.unwrap_or("hello") {|s| "Hi, #{s}" }
17
+ def unwrap_or(_)
18
+ if block_given?
19
+ yield unwrap
20
+ else
21
+ unwrap
22
+ end
23
+ end
24
+
25
+ # Invoke the block on the value, unwrapped. This method produces the wrapped
26
+ # value again, making it chainable. See `blank' for its companion.
27
+ #
28
+ # > w.present {|s| puts "Hello, #{s}" }.blank { puts "Do I know you?" }
29
+ def present(&block)
30
+ block.call(unwrap)
31
+ self
32
+ end
33
+
34
+ # Do nothing then produce the wrapped value, making it chainable. See
35
+ # `present' for its companion.
36
+ #
37
+ # > w.blank { puts "Symbol not found" }.present {|s| puts users[s]}
38
+ def blank(&ignored)
39
+ self
40
+ end
41
+
42
+ # Produce the singleton list with the unwrapped value as its only member.
43
+ #
44
+ # If a block is passed, it is run against the unwrapped value.
45
+ #
46
+ # This class mixes in Enumerable, which is controlled by this method.
47
+ #
48
+ # > w.each {|n| puts "Found #{n}" }
49
+ def each
50
+ yield unwrap if block_given?
51
+ [unwrap]
52
+ end
53
+
54
+ # The raw value. I doubt you need this method.
55
+ def unwrap
56
+ @value
57
+ end
58
+
59
+ # True; this is an instance of a wrapped value.
60
+ def present?
61
+ true
62
+ end
63
+
64
+ # False; this does not wrap a nil.
65
+ def blank?
66
+ false
67
+ end
68
+
69
+ # Run a block against the unwrapped value, producing the result of the block.
70
+ #
71
+ # > w.try {|n| n+1 }
72
+ #
73
+ # Also, you can use this like you would use >>= in Haskell. This and wrapped
74
+ # make it a monad.
75
+ #
76
+ # > w.try {|n| (n+1).wrapped }
77
+ def try
78
+ yield unwrap
79
+ end
80
+
81
+ # Run a block within the wrapper. This produces a wrapped value.
82
+ #
83
+ # > w.try {|n| n+1 }
84
+ #
85
+ # This makes it a functor.
86
+ def fmap
87
+ (yield unwrap).wrapped
88
+ end
89
+ end
@@ -0,0 +1,3 @@
1
+ module Wrapped
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1 @@
1
+ require 'wrapped'
@@ -0,0 +1,175 @@
1
+ require 'spec_helper'
2
+
3
+ describe Wrapped, 'conversion' do
4
+ let(:value) { 1 }
5
+ let(:just) { 1.wrapped }
6
+ let(:nothing) { nil.wrapped }
7
+
8
+ it "converts the value to a Present" do
9
+ just.should be_instance_of(Present)
10
+ end
11
+
12
+ it "converts the nil to a Blank" do
13
+ nothing.should be_instance_of(Blank)
14
+ end
15
+ end
16
+
17
+ describe Wrapped, 'accessing' do
18
+ let(:value) { 1 }
19
+ let(:just) { 1.wrapped }
20
+ let(:nothing) { nil.wrapped }
21
+
22
+ it 'produces the value of the wrapped object' do
23
+ just.unwrap.should == value
24
+ end
25
+
26
+ it 'raises an exception when called on the wrapped nil' do
27
+ expect { nothing.unwrap }.to raise_error(IndexError)
28
+ end
29
+ end
30
+
31
+ describe Wrapped, 'callbacks' do
32
+ let(:value) { 1 }
33
+ let(:just) { 1.wrapped }
34
+ let(:nothing) { nil.wrapped }
35
+
36
+ it 'calls the proper callback for a wrapped value' do
37
+ result = false
38
+ just.present {|v| result = v}
39
+ result.should be_true
40
+ end
41
+
42
+ it 'calls the proper callback for a wrapped nil' do
43
+ result = false
44
+ nothing.blank {result = true}
45
+ result.should be_true
46
+ end
47
+
48
+ it 'ignores the other callback for a wrapped value' do
49
+ result = true
50
+ just.blank { result = false }
51
+ result.should be_true
52
+ end
53
+
54
+
55
+ it 'ignores the other callback for a wrapped nil' do
56
+ result = true
57
+ nothing.present { result = false }
58
+ result.should be_true
59
+ end
60
+
61
+ it 'chains for wrapped values' do
62
+ result = false
63
+ just.present { result = true }.blank { result = false }
64
+ result.should be_true
65
+ end
66
+
67
+ it 'chains for wrapped nils' do
68
+ result = false
69
+ nothing.present { result = false }.blank { result = true }
70
+ result.should be_true
71
+ end
72
+ end
73
+
74
+ # This behavior is different from Haskell and Scala.
75
+ # It is done this way for consistency with Ruby.
76
+ # See the functor description later for `fmap'.
77
+ describe Wrapped, 'enumerable' do
78
+ let(:value) { 1 }
79
+ let(:just) { 1.wrapped }
80
+ let(:nothing) { nil.wrapped }
81
+
82
+ it 'acts over the value for #each on a wrapped value' do
83
+ result = -1
84
+ just.each {|v| result = v }
85
+ result.should == value
86
+ end
87
+
88
+ it 'produces a singleton array of the value for a wrapped value on #each' do
89
+ just.each.should == [value]
90
+ end
91
+
92
+ it 'skips the block for #each on a wrapped nil' do
93
+ result = -1
94
+ nothing.each {|v| result = v }
95
+ result.should == -1
96
+ end
97
+
98
+ it 'produces the empty array for a wrapped nil on #each' do
99
+ nothing.each.should be_empty
100
+ end
101
+
102
+ it 'maps over the value for a wrapped value' do
103
+ just.map {|n| n + 1}.should == [value+1]
104
+ end
105
+
106
+ it 'map produces the empty list for a wrapped nil' do
107
+ nothing.map {|n| n + 1}.should == []
108
+ end
109
+ end
110
+
111
+ describe Wrapped, 'queries' do
112
+ let(:value) { 1 }
113
+ let(:just) { 1.wrapped }
114
+ let(:nothing) { nil.wrapped }
115
+
116
+ it 'knows whether it is present' do
117
+ just.should be_present
118
+ nothing.should_not be_present
119
+ end
120
+
121
+ it 'knows whether it is blank' do
122
+ just.should_not be_blank
123
+ nothing.should be_blank
124
+ end
125
+ end
126
+
127
+ describe Wrapped, 'unwrap_or' do
128
+ let(:value) { 1 }
129
+ let(:just) { 1.wrapped }
130
+ let(:nothing) { nil.wrapped }
131
+
132
+ it 'produces the value for a wrapped value' do
133
+ just.unwrap_or(-1).should == value
134
+ end
135
+
136
+ it 'produces the default for a wrapped nil' do
137
+ nothing.unwrap_or(-1).should == -1
138
+ end
139
+
140
+ it 'produces the value of the block for a wrapped object' do
141
+ just.unwrap_or(-1) {|n| n+1}.should == value + 1
142
+ end
143
+
144
+ it 'produces the default for a wrapped nil even with a block' do
145
+ nothing.unwrap_or(-1) {2}.should == -1
146
+ end
147
+ end
148
+
149
+ describe Wrapped, 'monadic' do
150
+ let(:value) { 1 }
151
+ let(:just) { 1.wrapped }
152
+ let(:nothing) { nil.wrapped }
153
+
154
+ it 'produces the value from #try for a wrapped value' do
155
+ just.try {|n| (n+1).wrapped }.unwrap.should == value+1
156
+ end
157
+
158
+ it 'produces blank from #try for a wrapped nil' do
159
+ nothing.try {|n| (n+1).wrapped}.should be_blank
160
+ end
161
+ end
162
+
163
+ describe Wrapped, 'functor' do
164
+ let(:value) { 1 }
165
+ let(:just) { 1.wrapped }
166
+ let(:nothing) { nil.wrapped }
167
+
168
+ it 'unwraps, applies the block, then re-wraps for a wrapped value' do
169
+ just.fmap {|n| n+1}.unwrap.should == value+1
170
+ end
171
+
172
+ it 'produces the blank for a wrapped nil' do
173
+ nothing.fmap {|n| n+1}.should be_blank
174
+ end
175
+ end
data/wrapped.gemspec ADDED
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "wrapped/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "wrapped"
7
+ s.version = Wrapped::VERSION
8
+ s.authors = ["Mike Burns"]
9
+ s.email = ["mike@mike-burns.com"]
10
+ s.homepage = ""
11
+ s.summary = %q{The maybe functor for Ruby}
12
+ 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.}
13
+
14
+ s.files = `git ls-files`.split("\n")
15
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
17
+ s.require_paths = ["lib"]
18
+
19
+ s.add_development_dependency('rspec')
20
+ s.add_development_dependency('rake')
21
+ end
metadata ADDED
@@ -0,0 +1,105 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: wrapped
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Mike Burns
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-07-29 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: rspec
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ hash: 3
29
+ segments:
30
+ - 0
31
+ version: "0"
32
+ type: :development
33
+ version_requirements: *id001
34
+ - !ruby/object:Gem::Dependency
35
+ name: rake
36
+ prerelease: false
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ none: false
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ hash: 3
43
+ segments:
44
+ - 0
45
+ version: "0"
46
+ type: :development
47
+ version_requirements: *id002
48
+ description: 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.
49
+ email:
50
+ - mike@mike-burns.com
51
+ executables: []
52
+
53
+ extensions: []
54
+
55
+ extra_rdoc_files: []
56
+
57
+ files:
58
+ - .gitignore
59
+ - Gemfile
60
+ - LICENSE
61
+ - README.md
62
+ - Rakefile
63
+ - lib/wrapped.rb
64
+ - lib/wrapped/blank.rb
65
+ - lib/wrapped/injection.rb
66
+ - lib/wrapped/present.rb
67
+ - lib/wrapped/version.rb
68
+ - spec/spec_helper.rb
69
+ - spec/wrapped_spec.rb
70
+ - wrapped.gemspec
71
+ homepage: ""
72
+ licenses: []
73
+
74
+ post_install_message:
75
+ rdoc_options: []
76
+
77
+ require_paths:
78
+ - lib
79
+ required_ruby_version: !ruby/object:Gem::Requirement
80
+ none: false
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ hash: 3
85
+ segments:
86
+ - 0
87
+ version: "0"
88
+ required_rubygems_version: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ hash: 3
94
+ segments:
95
+ - 0
96
+ version: "0"
97
+ requirements: []
98
+
99
+ rubyforge_project:
100
+ rubygems_version: 1.7.2
101
+ signing_key:
102
+ specification_version: 3
103
+ summary: The maybe functor for Ruby
104
+ test_files: []
105
+