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 CHANGED
@@ -6,4 +6,5 @@ pkg/*
6
6
  html
7
7
  rdoc
8
8
  .yardoc
9
+ doc
9
10
 
data/.travis.yml ADDED
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.2
data/HISTORY.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # HISTORY
2
2
 
3
+ ## v0.3.0 (Apr 29, 2012)
4
+
5
+ - added Either#lift_to_a (and #lift)
6
+ - See full list @ https://github.com/ms-ati/rumonade/compare/v0.2.2...v0.3.0
7
+
3
8
  ## v0.2.2 (Apr 26, 2012)
4
9
 
5
10
  - added Either#+ to allow combining validations
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/0.2.1/file/README.rdoc]
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
- Using Option, one can transform _possibly_ _nil_ values in a _functional_ fashion, which can increase clarity while
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
@@ -1,6 +1,8 @@
1
1
  require "bundler/gem_tasks"
2
2
  require "rake/testtask"
3
3
 
4
+ task :default => [:test]
5
+
4
6
  Rake::TestTask.new(:test) do |test|
5
7
  test.libs << 'lib' << 'test'
6
8
  test.pattern = 'test/**/*_test.rb'
@@ -44,14 +44,47 @@ module Rumonade
44
44
  RightProjection.new(self)
45
45
  end
46
46
 
47
- # @param [Either] other the either to combine with
48
- # @return [Either] Returns a +Left+ with all Left values (if any), otherwise a +Right+ with all Right values
49
- def +(other)
50
- if [self, other].any?(&:left?)
51
- Left([self, other].map { |either| either.left.to_opt }.flatten)
52
- else
53
- Right([self, other].map { |either| either.right.to_opt }.flatten)
54
- end
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
@@ -1,3 +1,3 @@
1
1
  module Rumonade
2
- VERSION = "0.2.2"
2
+ VERSION = "0.3.0"
3
3
  end
data/rumonade.gemspec CHANGED
@@ -20,5 +20,5 @@ Gem::Specification.new do |s|
20
20
 
21
21
  # specify any dependencies here; for example:
22
22
  s.add_development_dependency "test-unit"
23
- #s.add_development_dependency "rr"
23
+ s.add_development_dependency "rake"
24
24
  end
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 test_plus_for_left_and_right
164
- assert_equal Left(["bad", "worse"]), Left("bad") + Right(:good) + Left("worse") + Right(:good)
165
- assert_equal Right([:good, :better]), Right(:good) + Right(:better)
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.2.2
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-27 00:00:00.000000000 Z
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