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