rumonade 0.2.2 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|