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 +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
|