rumonade 0.1.1 → 0.1.2

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