rumonade 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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