rumonade 0.2.2 → 0.3.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/.gitignore +1 -0
- data/.travis.yml +3 -0
- data/HISTORY.md +5 -0
- data/README.rdoc +34 -3
- data/Rakefile +2 -0
- data/lib/rumonade/either.rb +61 -8
- data/lib/rumonade/version.rb +1 -1
- data/rumonade.gemspec +1 -1
- data/test/either_test.rb +39 -3
- metadata +19 -2
data/.travis.yml
ADDED
data/HISTORY.md
CHANGED
data/README.rdoc
CHANGED
@@ -1,8 +1,10 @@
|
|
1
|
+
{<img src="https://secure.travis-ci.org/ms-ati/rumonade.png?branch=master" alt="Build Status" />}[http://travis-ci.org/ms-ati/rumonade]
|
2
|
+
|
1
3
|
= Rumonade[https://rubygems.org/gems/rumonade]
|
2
4
|
|
3
5
|
Project: github[http://github.com/ms-ati/rumonade]
|
4
6
|
|
5
|
-
Documentation: rubydoc.info[http://rubydoc.info/gems/rumonade/
|
7
|
+
Documentation: rubydoc.info[http://rubydoc.info/gems/rumonade/frames]
|
6
8
|
|
7
9
|
== 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
10
|
|
@@ -28,8 +30,7 @@ results. If this proves useful (and a good fit for Ruby), then more narrow funct
|
|
28
30
|
|
29
31
|
== Usage
|
30
32
|
|
31
|
-
|
32
|
-
eliminating opportunities for bugs:
|
33
|
+
==== {Rumonade::Option Option}: handle _possibly_ _nil_ values in a _functional_ fashion:
|
33
34
|
|
34
35
|
def format_date_in_march(time_or_date_or_nil)
|
35
36
|
Option(time_or_date_or_nil). # wraps possibly-nil value in an Option monad (Some or None)
|
@@ -48,6 +49,36 @@ Note:
|
|
48
49
|
* each step of the chained computations above are functionally isolated
|
49
50
|
* the value can notionally _start_ as nil, or _become_ nil during a computation, without effecting any other chained computations
|
50
51
|
|
52
|
+
==== {Rumonade::Either Either}: handle failures ({Rumonade::Left Left}) and successes ({Rumonade::Right Right}) in a _functional_ fashion:
|
53
|
+
|
54
|
+
def find_person(name)
|
55
|
+
case name
|
56
|
+
when /Jack/i, /John/i
|
57
|
+
Right(name.capitalize)
|
58
|
+
else
|
59
|
+
Left("No such person: #{name.capitalize}")
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# success looks like this:
|
64
|
+
find_person("Jack")
|
65
|
+
# => Right("Jack")
|
66
|
+
|
67
|
+
# failure looks like this:
|
68
|
+
find_person("Jill")
|
69
|
+
# => Left("No such person: Jill")
|
70
|
+
|
71
|
+
# on the 'happy path', we can functionally combine and transform successes:
|
72
|
+
(find_person("Jack").lift_to_a + find_person("John").lift_to_a).right.map { |*names| names.join(" and ") }
|
73
|
+
# => Right("Jack and John")
|
74
|
+
|
75
|
+
# while the failure cases are easily handled as well:
|
76
|
+
(find_person("Jack").lift_to_a +
|
77
|
+
find_person("John").lift_to_a +
|
78
|
+
find_person("Jill").lift_to_a +
|
79
|
+
find_person("Joan").lift_to_a).right.map { |*names| names.join(" and ") }
|
80
|
+
# => Left("No such person: Jill", "No such person: Joan")
|
81
|
+
|
51
82
|
(_more_ _examples_ _coming_ _soon_...)
|
52
83
|
|
53
84
|
== Approach
|
data/Rakefile
CHANGED
data/lib/rumonade/either.rb
CHANGED
@@ -44,14 +44,47 @@ module Rumonade
|
|
44
44
|
RightProjection.new(self)
|
45
45
|
end
|
46
46
|
|
47
|
-
#
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
47
|
+
# Default concatenation function used by {#+}
|
48
|
+
DEFAULT_CONCAT = lambda { |a,b| a + b }
|
49
|
+
|
50
|
+
# @param [Either] other the other +Either+ to concatenate
|
51
|
+
# @param [Hash] opts the options to concatenate with
|
52
|
+
# @option opts [Proc] :concat_left (DEFAULT_CONCAT) The function to concatenate +Left+ values
|
53
|
+
# @option opts [Proc] :concat_right (DEFAULT_CONCAT) the function to concatenate +Right+ values
|
54
|
+
# @yield [right_value] optional block to transform concatenated +Right+ values
|
55
|
+
# @yieldparam [Object] right_values the concatenated +Right+ values yielded to optional block
|
56
|
+
# @return [Either] if both are +Right+, returns +Right+ with +right_value+'s concatenated,
|
57
|
+
# otherwise a +Left+ with +left_value+'s concatenated
|
58
|
+
def +(other, opts = {})
|
59
|
+
opts = { :concat_left => DEFAULT_CONCAT, :concat_right => DEFAULT_CONCAT }.merge(opts)
|
60
|
+
result =
|
61
|
+
case self
|
62
|
+
when Left
|
63
|
+
case other
|
64
|
+
when Left then Left(opts[:concat_left].call(self.left_value, other.left_value))
|
65
|
+
when Right then Left(self.left_value)
|
66
|
+
end
|
67
|
+
when Right
|
68
|
+
case other
|
69
|
+
when Left then Left(other.left_value)
|
70
|
+
when Right then Right(opts[:concat_right].call(self.right_value, other.right_value))
|
71
|
+
end
|
72
|
+
end
|
73
|
+
if block_given? then result.right.map { |right_values| yield right_values } else result end
|
74
|
+
end
|
75
|
+
alias_method :concat, :+
|
76
|
+
|
77
|
+
# @return [Either] returns an +Either+ of the same type, with the +left_value+ or +right_value+
|
78
|
+
# lifted into an +Array+
|
79
|
+
def lift_to_a
|
80
|
+
lift(Array)
|
81
|
+
end
|
82
|
+
|
83
|
+
# @param [#unit] monad_class the {Monad} to lift the +Left+ or +Right+ value into
|
84
|
+
# @return [Either] returns an +Either+of the same type, with the +left_value+ or +right_value+
|
85
|
+
# lifted into +monad_class+
|
86
|
+
def lift(monad_class)
|
87
|
+
fold(lambda {|l| Left(monad_class.unit(l)) }, lambda {|r| Right(monad_class.unit(r))})
|
55
88
|
end
|
56
89
|
end
|
57
90
|
|
@@ -74,6 +107,11 @@ module Rumonade
|
|
74
107
|
def to_s
|
75
108
|
"Left(#{left_value})"
|
76
109
|
end
|
110
|
+
|
111
|
+
# @return [String] Returns a +String+ containing a human-readable representation of this object.
|
112
|
+
def inspect
|
113
|
+
"Left(#{left_value.inspect})"
|
114
|
+
end
|
77
115
|
end
|
78
116
|
|
79
117
|
# The right side of the disjoint union, as opposed to the Left side.
|
@@ -95,6 +133,11 @@ module Rumonade
|
|
95
133
|
def to_s
|
96
134
|
"Right(#{right_value})"
|
97
135
|
end
|
136
|
+
|
137
|
+
# @return [String] Returns a +String+ containing a human-readable representation of this object.
|
138
|
+
def inspect
|
139
|
+
"Right(#{right_value.inspect})"
|
140
|
+
end
|
98
141
|
end
|
99
142
|
|
100
143
|
# @param (see Left#initialize)
|
@@ -184,6 +227,11 @@ module Rumonade
|
|
184
227
|
def to_s
|
185
228
|
"LeftProjection(#{either_value})"
|
186
229
|
end
|
230
|
+
|
231
|
+
# @return [String] Returns a +String+ containing a human-readable representation of this object.
|
232
|
+
def inspect
|
233
|
+
"LeftProjection(#{either_value.inspect})"
|
234
|
+
end
|
187
235
|
end
|
188
236
|
|
189
237
|
# Projects an Either into a Right.
|
@@ -260,6 +308,11 @@ module Rumonade
|
|
260
308
|
def to_s
|
261
309
|
"RightProjection(#{either_value})"
|
262
310
|
end
|
311
|
+
|
312
|
+
# @return [String] Returns a +String+ containing a human-readable representation of this object.
|
313
|
+
def inspect
|
314
|
+
"RightProjection(#{either_value.inspect})"
|
315
|
+
end
|
263
316
|
end
|
264
317
|
end
|
265
318
|
end
|
data/lib/rumonade/version.rb
CHANGED
data/rumonade.gemspec
CHANGED
data/test/either_test.rb
CHANGED
@@ -160,8 +160,44 @@ class EitherTest < Test::Unit::TestCase
|
|
160
160
|
assert_equal "LeftProjection(Right(42))", Right(42).left.to_s
|
161
161
|
end
|
162
162
|
|
163
|
-
def
|
164
|
-
assert_equal Left(
|
165
|
-
assert_equal Right(
|
163
|
+
def test_inspect_for_left_and_right_and_their_projections
|
164
|
+
assert_equal "Left(\"error\")", Left("error").inspect
|
165
|
+
assert_equal "Right(\"success\")", Right("success").inspect
|
166
|
+
assert_equal "RightProjection(Left(\"error\"))", Left("error").right.inspect
|
167
|
+
assert_equal "LeftProjection(Right(\"success\"))", Right("success").left.inspect
|
168
|
+
end
|
169
|
+
|
170
|
+
def test_plus_concatenates_left_and_right_using_plus_operator
|
171
|
+
assert_equal Left("badworse"), Left("bad") + Right(1) + Left("worse") + Right(2)
|
172
|
+
assert_equal Left(["bad", "worse"]), Left(["bad"]) + Right(1) + Left(["worse"]) + Right(2)
|
173
|
+
assert_equal Right(3), Right(1) + Right(2)
|
174
|
+
end
|
175
|
+
|
176
|
+
def test_concat_concatenates_left_and_right_with_custom_concatenation_function
|
177
|
+
multiply = lambda { |a, b| a * b }
|
178
|
+
assert_equal Left(33), Left(3).concat(Left(11), :concat_left => multiply)
|
179
|
+
assert_equal Left(14), Left(3).concat(Left(11), :concat_right => multiply)
|
180
|
+
assert_equal Right(44), Right(4).concat(Right(11), :concat_right => multiply)
|
181
|
+
assert_equal Right(15), Right(4).concat(Right(11), :concat_left => multiply)
|
182
|
+
end
|
183
|
+
|
184
|
+
def test_lift_to_a_wraps_left_and_right_values_in_array
|
185
|
+
assert_equal Left(["error"]), Left("error").lift_to_a
|
186
|
+
assert_equal Right([42]), Right(42).lift_to_a
|
187
|
+
end
|
188
|
+
|
189
|
+
def test_plus_and_lift_to_a_work_together_to_concatenate_errors
|
190
|
+
assert_equal Left([1, 2]), Left(1).lift_to_a + Right(:a).lift_to_a + Left(2).lift_to_a + Right(:b).lift_to_a
|
191
|
+
assert_equal Right([:a, :b]), Right(:a).lift_to_a + Right(:b).lift_to_a
|
192
|
+
end
|
193
|
+
|
194
|
+
Person = Struct.new(:name, :age, :address)
|
195
|
+
|
196
|
+
def test_concat_maps_concatenated_right_values_through_a_block
|
197
|
+
assert_equal Right(Person.new("Joe", 23, ["123 Some St", "Boston"])),
|
198
|
+
Right(["Joe"]).concat(Right([23])).concat(Right([["123 Some St", "Boston"]])) { |n, a, addr| Person.new(n, a, addr) }
|
199
|
+
# this usage is equivalent, but since ruby can't pass a block to a binary operator, must use .right.map on result:
|
200
|
+
assert_equal Right(Person.new("Joe", 23, ["123 Some St", "Boston"])),
|
201
|
+
(Right(["Joe"]) + Right([23]) + Right([["123 Some St", "Boston"]])).right.map { |n, a, addr| Person.new(n, a, addr) }
|
166
202
|
end
|
167
203
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rumonade
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-04-
|
12
|
+
date: 2012-04-29 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: test-unit
|
@@ -27,6 +27,22 @@ dependencies:
|
|
27
27
|
- - ! '>='
|
28
28
|
- !ruby/object:Gem::Version
|
29
29
|
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rake
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
30
46
|
description: A Scala-inspired Monad library for Ruby, aiming to share the most common
|
31
47
|
idioms for folks working in both languages. Includes Option, Array, etc.
|
32
48
|
email:
|
@@ -36,6 +52,7 @@ extensions: []
|
|
36
52
|
extra_rdoc_files: []
|
37
53
|
files:
|
38
54
|
- .gitignore
|
55
|
+
- .travis.yml
|
39
56
|
- Gemfile
|
40
57
|
- HISTORY.md
|
41
58
|
- MIT-LICENSE.txt
|