rumonade 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.rdoc CHANGED
@@ -1,3 +1,7 @@
1
+ === 0.2.0 / 2011-10-18
2
+
3
+ * scala-like Either class w/ LeftProjection and RightProjection monads
4
+
1
5
  === 0.1.2 / 2011-10-12
2
6
 
3
7
  * Progress towards Either
data/README.rdoc CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Project: github[http://github.com/ms-ati/rumonade]
4
4
 
5
- Documentation: rubydoc.info[http://rubydoc.info/gems/rumonade/file/README.rdoc]
5
+ Documentation: rubydoc.info[http://rubydoc.info/gems/rumonade/0.2.0/file/README.rdoc]
6
6
 
7
7
  == A Ruby[http://www.ruby-lang.org] Monad[http://en.wikipedia.org/wiki/Monad_(functional_programming)] Library, Inspired by Scala[http://www.scala-lang.org]
8
8
 
@@ -71,5 +71,5 @@ The priorities for Rumonade are:
71
71
 
72
72
  == Status
73
73
 
74
- This code is in a very early state, but the Option monad is already present.
74
+ Option, Either, and Array are complete.
75
75
  Please try it out, and let me know what you think!
data/lib/rumonade.rb CHANGED
@@ -21,6 +21,7 @@
21
21
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
22
  #++
23
23
  require "rumonade/version"
24
+ require "rumonade/errors"
24
25
  require "rumonade/monad"
25
26
  require "rumonade/lazy_identity"
26
27
  require "rumonade/option"
@@ -60,6 +60,11 @@ module Rumonade
60
60
  def ==(other)
61
61
  other.is_a?(Left) && other.left_value == self.left_value
62
62
  end
63
+
64
+ # @return [String] Returns a +String+ representation of this object.
65
+ def to_s
66
+ "Left(#{left_value})"
67
+ end
63
68
  end
64
69
 
65
70
  # The right side of the disjoint union, as opposed to the Left side.
@@ -76,6 +81,11 @@ module Rumonade
76
81
  def ==(other)
77
82
  other.is_a?(Right) && other.right_value == self.right_value
78
83
  end
84
+
85
+ # @return [String] Returns a +String+ representation of this object.
86
+ def to_s
87
+ "Right(#{right_value})"
88
+ end
79
89
  end
80
90
 
81
91
  # @param (see Left#initialize)
@@ -93,6 +103,18 @@ module Rumonade
93
103
  class Either
94
104
  # Projects an Either into a Left.
95
105
  class LeftProjection
106
+ class << self
107
+ # @return [LeftProjection] Returns a +LeftProjection+ of the +Left+ of the given value
108
+ def unit(value)
109
+ self.new(Left(value))
110
+ end
111
+
112
+ # @return [LeftProjection] Returns the empty +LeftProjection+
113
+ def empty
114
+ self.new(Right(nil))
115
+ end
116
+ end
117
+
96
118
  # @param either_value [Object] the Either value to project
97
119
  def initialize(either_value)
98
120
  @either_value = either_value
@@ -101,18 +123,74 @@ module Rumonade
101
123
  # @return Returns the Either value
102
124
  attr_reader :either_value
103
125
 
126
+ # @return [Boolean] Returns +true+ if other is a +LeftProjection+ with an equal +Either+ value
104
127
  def ==(other)
105
128
  other.is_a?(LeftProjection) && other.either_value == self.either_value
106
129
  end
107
130
 
131
+ # Binds the given function across +Left+.
108
132
  def bind(lam = nil, &blk)
109
- !either_value.left? ? either_value : (lam || blk).call(either_value.left_value)
133
+ if !either_value.left? then either_value else (lam || blk).call(either_value.left_value) end
134
+ end
135
+
136
+ include Monad
137
+
138
+ # @return [Boolean] Returns +false+ if +Right+ or returns the result of the application of the given function to the +Left+ value.
139
+ def any?(lam = nil, &blk)
140
+ either_value.left? && bind(lam || blk)
141
+ end
142
+
143
+ # @return [Option] Returns +None+ if this is a +Right+ or if the given predicate does not hold for the +left+ value, otherwise, returns a +Some+ of +Left+.
144
+ def select(lam = nil, &blk)
145
+ Some(self).select { |lp| lp.any?(lam || blk) }.map { |lp| lp.either_value }
146
+ end
147
+
148
+ # @return [Boolean] Returns +true+ if +Right+ or returns the result of the application of the given function to the +Left+ value.
149
+ def all?(lam = nil, &blk)
150
+ !either_value.left? || bind(lam || blk)
151
+ end
152
+
153
+ # Returns the value from this +Left+ or raises +NoSuchElementException+ if this is a +Right+.
154
+ def get
155
+ if either_value.left? then either_value.left_value else raise NoSuchElementError end
156
+ end
157
+
158
+ # Returns the value from this +Left+ or the given argument if this is a +Right+.
159
+ def get_or_else(val_or_lam = nil, &blk)
160
+ v_or_f = val_or_lam || blk
161
+ if either_value.left? then either_value.left_value else (v_or_f.respond_to?(:call) ? v_or_f.call : v_or_f) end
162
+ end
163
+
164
+ # @return [Option] Returns a +Some+ containing the +Left+ value if it exists or a +None+ if this is a +Right+.
165
+ def to_opt
166
+ Option(get_or_else(nil))
167
+ end
168
+
169
+ # @return [Either] Maps the function argument through +Left+.
170
+ def map(lam = nil, &blk)
171
+ bind { |v| Left((lam || blk).call(v)) }
172
+ end
173
+
174
+ # @return [String] Returns a +String+ representation of this object.
175
+ def to_s
176
+ "LeftProjection(#{either_value})"
110
177
  end
111
- alias_method :flat_map, :bind
112
178
  end
113
179
 
114
180
  # Projects an Either into a Right.
115
181
  class RightProjection
182
+ class << self
183
+ # @return [RightProjection] Returns a +RightProjection+ of the +Right+ of the given value
184
+ def unit(value)
185
+ self.new(Right(value))
186
+ end
187
+
188
+ # @return [RightProjection] Returns the empty +RightProjection+
189
+ def empty
190
+ self.new(Left(nil))
191
+ end
192
+ end
193
+
116
194
  # @param either_value [Object] the Either value to project
117
195
  def initialize(either_value)
118
196
  @either_value = either_value
@@ -121,14 +199,58 @@ module Rumonade
121
199
  # @return Returns the Either value
122
200
  attr_reader :either_value
123
201
 
202
+ # @return [Boolean] Returns +true+ if other is a +RightProjection+ with an equal +Either+ value
124
203
  def ==(other)
125
204
  other.is_a?(RightProjection) && other.either_value == self.either_value
126
205
  end
127
206
 
207
+ # Binds the given function across +Right+.
128
208
  def bind(lam = nil, &blk)
129
- !either_value.right? ? either_value : (lam || blk).call(either_value.right_value)
209
+ if !either_value.right? then either_value else (lam || blk).call(either_value.right_value) end
210
+ end
211
+
212
+ include Monad
213
+
214
+ # @return [Boolean] Returns +false+ if +Left+ or returns the result of the application of the given function to the +Right+ value.
215
+ def any?(lam = nil, &blk)
216
+ either_value.right? && bind(lam || blk)
217
+ end
218
+
219
+ # @return [Option] Returns +None+ if this is a +Left+ or if the given predicate does not hold for the +Right+ value, otherwise, returns a +Some+ of +Right+.
220
+ def select(lam = nil, &blk)
221
+ Some(self).select { |lp| lp.any?(lam || blk) }.map { |lp| lp.either_value }
222
+ end
223
+
224
+ # @return [Boolean] Returns +true+ if +Left+ or returns the result of the application of the given function to the +Right+ value.
225
+ def all?(lam = nil, &blk)
226
+ !either_value.right? || bind(lam || blk)
227
+ end
228
+
229
+ # Returns the value from this +Right+ or raises +NoSuchElementException+ if this is a +Left+.
230
+ def get
231
+ if either_value.right? then either_value.right_value else raise NoSuchElementError end
232
+ end
233
+
234
+ # Returns the value from this +Right+ or the given argument if this is a +Left+.
235
+ def get_or_else(val_or_lam = nil, &blk)
236
+ v_or_f = val_or_lam || blk
237
+ if either_value.right? then either_value.right_value else (v_or_f.respond_to?(:call) ? v_or_f.call : v_or_f) end
238
+ end
239
+
240
+ # @return [Option] Returns a +Some+ containing the +Right+ value if it exists or a +None+ if this is a +Left+.
241
+ def to_opt
242
+ Option(get_or_else(nil))
243
+ end
244
+
245
+ # @return [Either] Maps the function argument through +Right+.
246
+ def map(lam = nil, &blk)
247
+ bind { |v| Right((lam || blk).call(v)) }
248
+ end
249
+
250
+ # @return [String] Returns a +String+ representation of this object.
251
+ def to_s
252
+ "RightProjection(#{either_value})"
130
253
  end
131
- alias_method :flat_map, :bind
132
254
  end
133
255
  end
134
256
  end
@@ -0,0 +1,6 @@
1
+ module Rumonade
2
+
3
+ # Exception raised on attempts to access non-existent wrapped values
4
+ class NoSuchElementError < RuntimeError; end
5
+
6
+ end
@@ -35,22 +35,24 @@ module Rumonade # :nodoc:
35
35
  # puts "No name value"
36
36
  # end
37
37
  #
38
+ # @abstract
38
39
  class Option
39
40
  class << self
40
- # Returns a new Option containing the given value
41
+ # @return [Option] Returns an +Option+ containing the given value
41
42
  def unit(value)
42
43
  Rumonade.Option(value)
43
44
  end
44
45
 
45
- # Returns the empty Option (None)
46
+ # @return [Option] Returns the empty +Option+
46
47
  def empty
47
48
  None
48
49
  end
49
50
  end
50
51
 
51
- def initialize # :nodoc:
52
+ def initialize
52
53
  raise(TypeError, "class Option is abstract; cannot be instantiated") if self.class == Option
53
54
  end
55
+ private :initialize
54
56
 
55
57
  # Returns None if None, or the result of executing the given block or lambda on the contents if Some
56
58
  def bind(lam = nil, &blk)
@@ -59,7 +61,7 @@ module Rumonade # :nodoc:
59
61
 
60
62
  include Monad
61
63
 
62
- # Returns +true+ if None, +false+ if Some
64
+ # @return [Boolean] Returns +true+ if +None+, +false+ if +Some+
63
65
  def empty?
64
66
  raise(NotImplementedError)
65
67
  end
@@ -89,6 +91,7 @@ module Rumonade # :nodoc:
89
91
 
90
92
  attr_reader :value # :nodoc:
91
93
 
94
+ # @return (see Option#empty?)
92
95
  def empty?
93
96
  false
94
97
  end
@@ -106,6 +109,7 @@ module Rumonade # :nodoc:
106
109
  class NoneClass < Option
107
110
  include Singleton
108
111
 
112
+ # @return (see Option#empty?)
109
113
  def empty?
110
114
  true
111
115
  end
@@ -119,15 +123,12 @@ module Rumonade # :nodoc:
119
123
  end
120
124
  end
121
125
 
122
- # Exception raised on attempts to access the value of None
123
- class NoSuchElementError < RuntimeError; end
124
-
125
126
  # Returns an Option wrapping the given value: Some if non-nil, None if nil
126
127
  def Option(value)
127
128
  value.nil? ? None : Some(value)
128
129
  end
129
130
 
130
- # Returns a Some wrapping the given value, for convenience
131
+ # @return [Some] Returns a +Some+ wrapping the given value, for convenience
131
132
  def Some(value)
132
133
  Some.new(value)
133
134
  end
@@ -1,3 +1,3 @@
1
1
  module Rumonade
2
- VERSION = "0.1.2"
2
+ VERSION = "0.2.0"
3
3
  end
data/test/either_test.rb CHANGED
@@ -50,4 +50,113 @@ class EitherTest < Test::Unit::TestCase
50
50
  assert_equal Right("ERROR"), Left("error").left.flat_map { |n| Right(n.upcase) }
51
51
  assert_equal Left("error"), Left("error").right.flat_map { |n| Right(n.upcase) }
52
52
  end
53
+
54
+ def test_any_predicate_for_left_and_right_projections_returns_true_if_correct_type_and_block_returns_true
55
+ assert Left("error").left.any? { |s| s == "error" }
56
+ assert !Left("error").left.any? { |s| s != "error" }
57
+ assert !Left("error").right.any? { |s| s == "error" }
58
+
59
+ assert Right(42).right.any? { |n| n == 42 }
60
+ assert !Right(42).right.any? { |n| n != 42 }
61
+ assert !Right(42).left.any? { |n| n == 42 }
62
+ end
63
+
64
+ def test_select_for_left_and_right_projects_returns_option_of_either_if_correct_type_and_block_returns_true
65
+ assert_equal Some(Left("error")), Left("error").left.select { |s| s == "error" }
66
+ assert_equal None, Left("error").left.select { |s| s != "error" }
67
+ assert_equal None, Left("error").right.select { |s| s == "error" }
68
+
69
+ assert_equal Some(Right(42)), Right(42).right.select { |n| n == 42 }
70
+ assert_equal None, Right(42).right.select { |n| n != 42 }
71
+ assert_equal None, Right(42).left.select { |n| n == 42 }
72
+ end
73
+
74
+ def test_all_predicate_for_left_and_right_projections_returns_true_if_correct_type_and_block_returns_true
75
+ assert Left("error").left.all? { |s| s == "error" }
76
+ assert !Left("error").left.all? { |s| s != "error" }
77
+ assert Left("error").right.all? { |s| s == "error" }
78
+
79
+ assert Right(42).right.all? { |n| n == 42 }
80
+ assert !Right(42).right.all? { |n| n != 42 }
81
+ assert Right(42).left.all? { |n| n == 42 }
82
+ end
83
+
84
+ def test_each_for_left_and_right_projections_executes_block_if_correct_type
85
+ def side_effect_occurred_on_each(projection)
86
+ side_effect_occurred = false
87
+ projection.each { |s| side_effect_occurred = true }
88
+ side_effect_occurred
89
+ end
90
+
91
+ assert side_effect_occurred_on_each(Left("error").left)
92
+ assert !side_effect_occurred_on_each(Left("error").right)
93
+
94
+ assert side_effect_occurred_on_each(Right(42).right)
95
+ assert !side_effect_occurred_on_each(Right(42).left)
96
+ end
97
+
98
+ def test_unit_for_left_and_right_projections
99
+ assert_equal Left("error").left, Either::LeftProjection.unit("error")
100
+ assert_equal Right(42).right, Either::RightProjection.unit(42)
101
+ end
102
+
103
+ def test_empty_for_left_and_right_projections
104
+ assert_equal Right(nil).left, Either::LeftProjection.empty
105
+ assert_equal Left(nil).right, Either::RightProjection.empty
106
+ end
107
+
108
+ def test_monad_axioms_for_left_and_right_projections
109
+ assert_monad_axiom_1(Either::LeftProjection, "error", lambda { |x| Left(x * 2).left })
110
+ assert_monad_axiom_2(Left("error").left)
111
+ assert_monad_axiom_3(Left("error").left, lambda { |x| Left(x * 2).left }, lambda { |x| Left(x * 5).left })
112
+
113
+ assert_monad_axiom_1(Either::RightProjection, 42, lambda { |x| Right(x * 2).right })
114
+ assert_monad_axiom_2(Right(42).right)
115
+ assert_monad_axiom_3(Right(42).right, lambda { |x| Right(x * 2).right }, lambda { |x| Right(x * 5).right })
116
+ end
117
+
118
+ def test_get_for_left_and_right_projections_returns_value_if_correct_type_or_raises
119
+ assert_equal "error", Left("error").left.get
120
+ assert_raises(NoSuchElementError) { Left("error").right.get }
121
+ assert_equal 42, Right(42).right.get
122
+ assert_raises(NoSuchElementError) { Right(42).left.get }
123
+ end
124
+
125
+ def test_get_or_else_for_left_and_right_projections_returns_value_if_correct_type_or_returns_value_or_executes_block
126
+ assert_equal "error", Left("error").left.get_or_else(:other_value)
127
+ assert_equal :other_value, Left("error").right.get_or_else(:other_value)
128
+ assert_equal :value_of_block, Left("error").right.get_or_else(lambda { :value_of_block })
129
+
130
+ assert_equal 42, Right(42).right.get_or_else(:other_value)
131
+ assert_equal :other_value, Right(42).left.get_or_else(:other_value)
132
+ assert_equal :value_of_block, Right(42).left.get_or_else(lambda { :value_of_block })
133
+ end
134
+
135
+ def test_to_opt_for_left_and_right_projections_returns_Some_if_correct_type_or_None
136
+ assert_equal Some("error"), Left("error").left.to_opt
137
+ assert_equal None, Left("error").right.to_opt
138
+ assert_equal Some(42), Right(42).right.to_opt
139
+ assert_equal None, Right(42).left.to_opt
140
+ end
141
+
142
+ def test_to_a_for_left_and_right_projections_returns_single_element_Array_if_correct_type_or_zero_element_Array
143
+ assert_equal ["error"], Left("error").left.to_a
144
+ assert_equal [], Left("error").right.to_a
145
+ assert_equal [42], Right(42).right.to_a
146
+ assert_equal [], Right(42).left.to_a
147
+ end
148
+
149
+ def test_map_for_left_and_right_projections_returns_same_projection_type_of_new_value_if_correct_type
150
+ assert_equal Left(:ERROR), Left("error").left.map { |s| s.upcase.to_sym }
151
+ assert_equal Left("error"), Left("error").right.map { |s| s.upcase.to_sym }
152
+ assert_equal Right(420), Right(42).right.map { |s| s * 10 }
153
+ assert_equal Right(42), Right(42).left.map { |s| s * 10 }
154
+ end
155
+
156
+ def test_to_s_for_left_and_right_and_their_projections
157
+ assert_equal "Left(error)", Left("error").to_s
158
+ assert_equal "Right(42)", Right(42).to_s
159
+ assert_equal "RightProjection(Left(error))", Left("error").right.to_s
160
+ assert_equal "LeftProjection(Right(42))", Right(42).left.to_s
161
+ end
53
162
  end
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: rumonade
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 0.1.2
5
+ version: 0.2.0
6
6
  platform: ruby
7
7
  authors:
8
8
  - Marc Siegel
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2011-10-13 00:00:00 Z
13
+ date: 2011-10-18 00:00:00 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: test-unit
@@ -42,6 +42,7 @@ files:
42
42
  - lib/rumonade.rb
43
43
  - lib/rumonade/array.rb
44
44
  - lib/rumonade/either.rb
45
+ - lib/rumonade/errors.rb
45
46
  - lib/rumonade/lazy_identity.rb
46
47
  - lib/rumonade/monad.rb
47
48
  - lib/rumonade/option.rb