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 +1 -0
- data/CHANGELOG.rdoc +5 -0
- data/README.rdoc +24 -16
- data/Rakefile +0 -18
- data/lib/rumonade.rb +1 -0
- data/lib/rumonade/array.rb +2 -2
- data/lib/rumonade/either.rb +134 -0
- data/lib/rumonade/version.rb +1 -1
- data/rumonade.gemspec +1 -2
- data/test/either_test.rb +53 -0
- data/test/test_helper.rb +0 -1
- metadata +17 -4
data/CHANGELOG.rdoc
CHANGED
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
|
-
* (
|
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
|
-
|
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).
|
36
|
-
|
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('
|
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
|
-
|
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.,
|
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
data/lib/rumonade/array.rb
CHANGED
@@ -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
|
-
|
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
|
data/lib/rumonade/version.rb
CHANGED
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
|
-
|
23
|
-
#s.add_development_dependency "test-unit"
|
22
|
+
s.add_development_dependency "test-unit"
|
24
23
|
#s.add_development_dependency "rr"
|
25
24
|
end
|
data/test/either_test.rb
ADDED
@@ -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
metadata
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
name: rumonade
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease:
|
5
|
-
version: 0.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-
|
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
|