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 +4 -0
- data/README.rdoc +2 -2
- data/lib/rumonade.rb +1 -0
- data/lib/rumonade/either.rb +126 -4
- data/lib/rumonade/errors.rb +6 -0
- data/lib/rumonade/option.rb +9 -8
- data/lib/rumonade/version.rb +1 -1
- data/test/either_test.rb +109 -0
- metadata +3 -2
data/CHANGELOG.rdoc
CHANGED
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
|
-
|
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
data/lib/rumonade/either.rb
CHANGED
@@ -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?
|
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?
|
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
|
data/lib/rumonade/option.rb
CHANGED
@@ -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
|
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
|
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
|
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
|
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
|
data/lib/rumonade/version.rb
CHANGED
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.
|
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
|
+
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
|