rumonade 0.1.1 → 0.1.2

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
@@ -5,4 +5,5 @@ pkg/*
5
5
  .idea
6
6
  html
7
7
  rdoc
8
+ .yardoc
8
9
 
data/CHANGELOG.rdoc CHANGED
@@ -1,3 +1,8 @@
1
+ === 0.1.2 / 2011-10-12
2
+
3
+ * Progress towards Either
4
+ * changed documentation to yard from rdoc
5
+
1
6
  === 0.1.1 / 2011-09-19
2
7
 
3
8
  * Maintenance
data/README.rdoc CHANGED
@@ -1,5 +1,9 @@
1
1
  = Rumonade[https://rubygems.org/gems/rumonade]
2
2
 
3
+ Project: github[http://github.com/ms-ati/rumonade]
4
+
5
+ Documentation: rubydoc.info[http://rubydoc.info/gems/rumonade/file/README.rdoc]
6
+
3
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]
4
8
 
5
9
  Are you working in both the Scala[http://www.scala-lang.org] and Ruby[http://www.ruby-lang.org] worlds,
@@ -8,11 +12,11 @@ monads[http://james-iry.blogspot.com/2007/09/monads-are-elephants-part-1.html] i
8
12
  Then Rumonade is for you.
9
13
 
10
14
  The goal of this library is to make the most common and useful Scala monadic idioms available in Ruby via the following classes:
11
- * Rumonade::Option
12
- * Array
13
- * Either
14
- * Hash
15
- * (more TBD)
15
+ * {Rumonade::Option Option}
16
+ * {Rumonade::ArrayExtensions Array}
17
+ * {Rumonade::Either Either}
18
+ * Hash -- _coming_ _soon!_
19
+ * (_more_ _TBD_)
16
20
 
17
21
  Syntactic support for scala-like for-comprehensions[http://www.scala-lang.org/node/111] will be implemented
18
22
  as a sequence of calls to #flat_map, #select, etc, modelling Scala's
@@ -24,23 +28,27 @@ results. If this proves useful (and a good fit for Ruby), then more narrow funct
24
28
 
25
29
  == Usage
26
30
 
27
- You can transform possibly nil values in a functional fashion, which many find more clear and elegant:
28
-
29
- require 'date'
30
- require 'time'
31
- require 'rumonade'
31
+ Using Option, one can transform _possibly_ _nil_ values in a _functional_ fashion, which can increase clarity while
32
+ eliminating opportunities for bugs:
32
33
 
33
34
  def format_date_in_march(time_or_date_or_nil)
34
- Option(time_or_date_or_nil).
35
- map(&:to_date).select {|d| d.month == 3}.
36
- map(&:to_s).map {|s| s.gsub('-', '')}.get_or_else("not in march!")
35
+ Option(time_or_date_or_nil). # wraps possibly-nil value in an Option monad (Some or None)
36
+ map(&:to_date). # transforms a contained Time value into a Date value
37
+ select {|d| d.month == 3}. # filters out non-matching Date values (Some becomes None)
38
+ map(&:to_s). # transforms a contained Date value into a String value
39
+ map {|s| s.gsub('-', '')}. # transforms a contained String value by removing '-'
40
+ get_or_else("not in march!") # returns the contained value, or the alternative if None
37
41
  end
38
42
 
39
43
  format_date_in_march(nil) # => "not in march!"
40
- format_date_in_march(Time.parse('2011-01-01 12:34')) # => "not in march!"
44
+ format_date_in_march(Time.parse('2009-01-01 01:02')) # => "not in march!"
41
45
  format_date_in_march(Time.parse('2011-03-21 12:34')) # => "20110321"
42
46
 
43
- (more examples coming soon...)
47
+ Note:
48
+ * each step of the chained computations above are functionally isolated
49
+ * the value can notionally _start_ as nil, or _become_ nil during a computation, without effecting any other chained computations
50
+
51
+ (_more_ _examples_ _coming_ _soon_...)
44
52
 
45
53
  == Approach
46
54
 
@@ -54,7 +62,7 @@ Rumonade wants to be a practical drop-in Monad solution that will fit well into
54
62
 
55
63
  The priorities for Rumonade are:
56
64
  1. Practical usability in day-to-day Ruby
57
- * <b>don't</b> mess up normal idioms of the language (e.g., Array#map)
65
+ * <b>don't</b> mess up normal idioms of the language (e.g., Hash#map)
58
66
  * <b>don't</b> slow down normal idioms of the language (e.g., Array#map)
59
67
  2. Rubyish-ness of usage
60
68
  * Monad is a mix-in, requiring methods self.unit and #bind be implemented by target classes
data/Rakefile CHANGED
@@ -5,21 +5,3 @@ Rake::TestTask.new(:test) do |test|
5
5
  test.libs << 'lib' << 'test'
6
6
  test.pattern = 'test/**/*_test.rb'
7
7
  end
8
-
9
- #require "rdoc/task"
10
- #RDoc::Task.new do |rdoc|
11
- # rdoc.main = "README.rdoc"
12
- # rdoc.rdoc_files.include("README.rdoc", "CHANGELOG.rdoc", "lib/**/*.rb")
13
- #end
14
-
15
- require 'rake/rdoctask'
16
- Rake::RDocTask.new do |rdoc|
17
- require File.expand_path("../lib/rumonade/version", __FILE__)
18
- rdoc.rdoc_dir = 'rdoc'
19
- rdoc.options << '--line-numbers' << '--inline-source'
20
- rdoc.title = "Rumonade #{Rumonade::VERSION}"
21
- rdoc.main = 'README.rdoc'
22
- rdoc.rdoc_files.include('README.rdoc')
23
- rdoc.rdoc_files.include('CHANGELOG.rdoc')
24
- rdoc.rdoc_files.include('lib/**/*.rb')
25
- end
data/lib/rumonade.rb CHANGED
@@ -25,6 +25,7 @@ require "rumonade/monad"
25
25
  require "rumonade/lazy_identity"
26
26
  require "rumonade/option"
27
27
  require "rumonade/array"
28
+ require "rumonade/either"
28
29
 
29
30
  # = Rumonade
30
31
  #
@@ -1,6 +1,7 @@
1
1
  require 'rumonade/monad'
2
2
 
3
3
  module Rumonade
4
+ # TODO: Document use of Array as a Monad
4
5
  module ArrayExtensions
5
6
  module ClassMethods
6
7
  def unit(value)
@@ -14,8 +15,7 @@ module Rumonade
14
15
 
15
16
  module InstanceMethods
16
17
  def bind(lam = nil, &blk)
17
- f = lam || blk
18
- inject([]) { |arr, elt| arr + f.call(elt).to_a }
18
+ inject(self.class.empty) { |arr, elt| arr + (lam || blk).call(elt).to_a }
19
19
  end
20
20
  end
21
21
  end
@@ -0,0 +1,134 @@
1
+ require 'singleton'
2
+ require 'rumonade/monad'
3
+
4
+ module Rumonade
5
+ # Represents a value of one of two possible types (a disjoint union).
6
+ # The data constructors {Rumonade::Left} and {Rumonade::Right} represent the two possible values.
7
+ # The +Either+ type is often used as an alternative to {Rumonade::Option} where {Rumonade::Left} represents
8
+ # failure (by convention) and {Rumonade::Right} is akin to {Rumonade::Some}.
9
+ # @abstract
10
+ class Either
11
+ def initialize
12
+ raise(TypeError, "class Either is abstract; cannot be instantiated") if self.class == Either
13
+ end
14
+ private :initialize
15
+
16
+ # @return [Boolean] Returns +true+ if this is a {Rumonade::Left}, +false+ otherwise.
17
+ def left?
18
+ is_a?(Left)
19
+ end
20
+
21
+ # @return [Boolean] Returns +true+ if this is a {Rumonade::Right}, +false+ otherwise.
22
+ def right?
23
+ is_a?(Right)
24
+ end
25
+
26
+ # @return [Boolean] If this is a Left, then return the left value in Right or vice versa.
27
+ def swap
28
+ if left? then Right(left_value) else Left(right_value) end
29
+ end
30
+
31
+ # @param [Proc] function_of_left_value the function to apply if this is a Left
32
+ # @param [Proc] function_of_right_value the function to apply if this is a Right
33
+ # @return Returns the results of applying the function
34
+ def fold(function_of_left_value, function_of_right_value)
35
+ if left? then function_of_left_value.call(left_value) else function_of_right_value.call(right_value) end
36
+ end
37
+
38
+ # @return [LeftProjection] Projects this Either as a Left.
39
+ def left
40
+ LeftProjection.new(self)
41
+ end
42
+
43
+ # @return [RightProjection] Projects this Either as a Right.
44
+ def right
45
+ RightProjection.new(self)
46
+ end
47
+ end
48
+
49
+ # The left side of the disjoint union, as opposed to the Right side.
50
+ class Left < Either
51
+ # @param left_value the value to store in a +Left+, usually representing a failure result
52
+ def initialize(left_value)
53
+ @left_value = left_value
54
+ end
55
+
56
+ # @return Returns the left value
57
+ attr_reader :left_value
58
+
59
+ # @return [Boolean] Returns +true+ if other is a +Left+ with an equal left value
60
+ def ==(other)
61
+ other.is_a?(Left) && other.left_value == self.left_value
62
+ end
63
+ end
64
+
65
+ # The right side of the disjoint union, as opposed to the Left side.
66
+ class Right < Either
67
+ # @param right_value the value to store in a +Right+, usually representing a success result
68
+ def initialize(right_value)
69
+ @right_value = right_value
70
+ end
71
+
72
+ # @return Returns the right value
73
+ attr_reader :right_value
74
+
75
+ # @return [Boolean] Returns +true+ if other is a +Right+ with an equal right value
76
+ def ==(other)
77
+ other.is_a?(Right) && other.right_value == self.right_value
78
+ end
79
+ end
80
+
81
+ # @param (see Left#initialize)
82
+ # @return [Left]
83
+ def Left(left_value)
84
+ Left.new(left_value)
85
+ end
86
+
87
+ # @param (see Right#initialize)
88
+ # @return [Right]
89
+ def Right(right_value)
90
+ Right.new(right_value)
91
+ end
92
+
93
+ class Either
94
+ # Projects an Either into a Left.
95
+ class LeftProjection
96
+ # @param either_value [Object] the Either value to project
97
+ def initialize(either_value)
98
+ @either_value = either_value
99
+ end
100
+
101
+ # @return Returns the Either value
102
+ attr_reader :either_value
103
+
104
+ def ==(other)
105
+ other.is_a?(LeftProjection) && other.either_value == self.either_value
106
+ end
107
+
108
+ def bind(lam = nil, &blk)
109
+ !either_value.left? ? either_value : (lam || blk).call(either_value.left_value)
110
+ end
111
+ alias_method :flat_map, :bind
112
+ end
113
+
114
+ # Projects an Either into a Right.
115
+ class RightProjection
116
+ # @param either_value [Object] the Either value to project
117
+ def initialize(either_value)
118
+ @either_value = either_value
119
+ end
120
+
121
+ # @return Returns the Either value
122
+ attr_reader :either_value
123
+
124
+ def ==(other)
125
+ other.is_a?(RightProjection) && other.either_value == self.either_value
126
+ end
127
+
128
+ def bind(lam = nil, &blk)
129
+ !either_value.right? ? either_value : (lam || blk).call(either_value.right_value)
130
+ end
131
+ alias_method :flat_map, :bind
132
+ end
133
+ end
134
+ end
@@ -1,3 +1,3 @@
1
1
  module Rumonade
2
- VERSION = "0.1.1"
2
+ VERSION = "0.1.2"
3
3
  end
data/rumonade.gemspec CHANGED
@@ -19,7 +19,6 @@ Gem::Specification.new do |s|
19
19
  s.require_paths = ["lib"]
20
20
 
21
21
  # specify any dependencies here; for example:
22
- #s.add_development_dependency "rdoc"
23
- #s.add_development_dependency "test-unit"
22
+ s.add_development_dependency "test-unit"
24
23
  #s.add_development_dependency "rr"
25
24
  end
@@ -0,0 +1,53 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/test_helper')
2
+
3
+ class EitherTest < Test::Unit::TestCase
4
+ include Rumonade
5
+ include MonadAxiomTestHelpers
6
+
7
+ def test_when_either_constructor_raises
8
+ assert_raise(TypeError) { Either.new }
9
+ end
10
+
11
+ def test_when_left_or_right_returns_new_left_or_right
12
+ assert_equal Left.new("error"), Left("error")
13
+ assert_equal Right.new(42), Right(42)
14
+ end
15
+
16
+ def test_predicates_for_left_and_right
17
+ assert Left("error").left?
18
+ assert !Right(42).left?
19
+ assert Right(42).right?
20
+ assert !Left("error").right?
21
+ end
22
+
23
+ def test_swap_for_left_and_right
24
+ assert_equal Left(42), Right(42).swap
25
+ assert_equal Right("error"), Left("error").swap
26
+ end
27
+
28
+ def test_fold_for_left_and_right
29
+ times_two = lambda { |v| v * 2 }
30
+ times_ten = lambda { |v| v * 10 }
31
+ assert_equal "errorerror", Left("error").fold(times_two, times_ten)
32
+ assert_equal 420, Right(42).fold(times_two, times_ten)
33
+ end
34
+
35
+ def test_projections_for_left_and_right
36
+ assert_equal Either::LeftProjection.new(Left("error")), Left("error").left
37
+ assert_equal Either::RightProjection.new(Left("error")), Left("error").right
38
+ assert_equal Either::LeftProjection.new(Right(42)), Right(42).left
39
+ assert_equal Either::RightProjection.new(Right(42)), Right(42).right
40
+
41
+ assert_not_equal Either::LeftProjection.new(Left("error")), Left("error").right
42
+ assert_not_equal Either::RightProjection.new(Left("error")), Left("error").left
43
+ assert_not_equal Either::LeftProjection.new(Right(42)), Right(42).right
44
+ assert_not_equal Either::RightProjection.new(Right(42)), Right(42).left
45
+ end
46
+
47
+ def test_flat_map_for_left_and_right_projections_returns_eithers
48
+ assert_equal Left("42"), Right(42).right.flat_map { |n| Left(n.to_s) }
49
+ assert_equal Right(42), Right(42).left.flat_map { |n| Left(n.to_s) }
50
+ assert_equal Right("ERROR"), Left("error").left.flat_map { |n| Right(n.upcase) }
51
+ assert_equal Left("error"), Left("error").right.flat_map { |n| Right(n.upcase) }
52
+ end
53
+ end
data/test/test_helper.rb CHANGED
@@ -1,7 +1,6 @@
1
1
  $:.unshift(File.dirname(File.dirname(File.expand_path(__FILE__))) + '/lib')
2
2
  require "rubygems"
3
3
  require "test/unit"
4
- require "rr"
5
4
  require "rumonade"
6
5
 
7
6
  # see http://stackoverflow.com/questions/2709361/monad-equivalent-in-ruby
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: rumonade
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 0.1.1
5
+ version: 0.1.2
6
6
  platform: ruby
7
7
  authors:
8
8
  - Marc Siegel
@@ -10,9 +10,19 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2011-09-20 00:00:00 Z
14
- dependencies: []
15
-
13
+ date: 2011-10-13 00:00:00 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: test-unit
17
+ prerelease: false
18
+ requirement: &id001 !ruby/object:Gem::Requirement
19
+ none: false
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ type: :development
25
+ version_requirements: *id001
16
26
  description: A Scala-inspired Monad library for Ruby, aiming to share the most common idioms for folks working in both languages. Includes Option, Array, etc.
17
27
  email:
18
28
  - marc@usainnov.com
@@ -31,12 +41,14 @@ files:
31
41
  - Rakefile
32
42
  - lib/rumonade.rb
33
43
  - lib/rumonade/array.rb
44
+ - lib/rumonade/either.rb
34
45
  - lib/rumonade/lazy_identity.rb
35
46
  - lib/rumonade/monad.rb
36
47
  - lib/rumonade/option.rb
37
48
  - lib/rumonade/version.rb
38
49
  - rumonade.gemspec
39
50
  - test/array_test.rb
51
+ - test/either_test.rb
40
52
  - test/lazy_identity_test.rb
41
53
  - test/option_test.rb
42
54
  - test/test_helper.rb
@@ -69,6 +81,7 @@ specification_version: 3
69
81
  summary: A Scala-inspired Monad library for Ruby
70
82
  test_files:
71
83
  - test/array_test.rb
84
+ - test/either_test.rb
72
85
  - test/lazy_identity_test.rb
73
86
  - test/option_test.rb
74
87
  - test/test_helper.rb