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