rumonade 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +4 -1
- data/HISTORY.md +5 -0
- data/MIT-LICENSE.txt +1 -1
- data/README.rdoc +42 -15
- data/lib/rumonade.rb +1 -0
- data/lib/rumonade/array.rb +3 -0
- data/lib/rumonade/either.rb +4 -0
- data/lib/rumonade/hash.rb +35 -0
- data/lib/rumonade/monad.rb +32 -9
- data/lib/rumonade/version.rb +1 -1
- data/test/hash_test.rb +61 -0
- metadata +6 -4
data/.travis.yml
CHANGED
data/HISTORY.md
CHANGED
data/MIT-LICENSE.txt
CHANGED
data/README.rdoc
CHANGED
@@ -2,9 +2,9 @@
|
|
2
2
|
|
3
3
|
= Rumonade[https://rubygems.org/gems/rumonade]
|
4
4
|
|
5
|
-
|
5
|
+
*_Project_*: github[http://github.com/ms-ati/rumonade]
|
6
6
|
|
7
|
-
|
7
|
+
*_Documentation_*: rubydoc.info[http://rubydoc.info/gems/rumonade/frames]
|
8
8
|
|
9
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]
|
10
10
|
|
@@ -17,20 +17,20 @@ The goal of this library is to make the most common and useful Scala monadic idi
|
|
17
17
|
* {Rumonade::Option Option}
|
18
18
|
* {Rumonade::ArrayExtensions Array}
|
19
19
|
* {Rumonade::Either Either}
|
20
|
-
* Hash
|
20
|
+
* {Rumonade::Hash Hash}
|
21
21
|
* (_more_ _TBD_)
|
22
22
|
|
23
23
|
Syntactic support for scala-like for-comprehensions[http://www.scala-lang.org/node/111] will be implemented
|
24
|
-
as a sequence of calls to #flat_map, #select, etc,
|
24
|
+
as a sequence of calls to #flat_map, #select, etc, modeling Scala's
|
25
25
|
approach[http://stackoverflow.com/questions/3754089/scala-for-comprehension/3754568#3754568].
|
26
26
|
|
27
27
|
Support for an all_catch[http://www.scala-lang.org/archives/downloads/distrib/files/nightly/docs/library/scala/util/control/Exception$.html]
|
28
28
|
idiom will be implemented to turn blocks which might throw exceptions into Option or Either
|
29
29
|
results. If this proves useful (and a good fit for Ruby), then more narrow functional catchers can be implemented as well.
|
30
30
|
|
31
|
-
== Usage
|
31
|
+
== Usage Examples
|
32
32
|
|
33
|
-
|
33
|
+
=== {Rumonade::Option Option}: handle _possibly_ _nil_ values in a _functional_ fashion:
|
34
34
|
|
35
35
|
def format_date_in_march(time_or_date_or_nil)
|
36
36
|
Option(time_or_date_or_nil). # wraps possibly-nil value in an Option monad (Some or None)
|
@@ -49,7 +49,8 @@ Note:
|
|
49
49
|
* each step of the chained computations above are functionally isolated
|
50
50
|
* the value can notionally _start_ as nil, or _become_ nil during a computation, without effecting any other chained computations
|
51
51
|
|
52
|
-
|
52
|
+
---
|
53
|
+
=== {Rumonade::Either Either}: handle failures ({Rumonade::Left Left}) and successes ({Rumonade::Right Right}) in a _functional_ fashion:
|
53
54
|
|
54
55
|
def find_person(name)
|
55
56
|
case name
|
@@ -68,16 +69,39 @@ Note:
|
|
68
69
|
find_person("Jill")
|
69
70
|
# => Left("No such person: Jill")
|
70
71
|
|
71
|
-
#
|
72
|
-
|
72
|
+
# lift the contained values into Array, in order to combine them:
|
73
|
+
find_person("Joan").lift_to_a
|
74
|
+
# => Left(["No such person: Joan"])
|
75
|
+
|
76
|
+
# on the 'happy path', combine and transform successes into a single success result:
|
77
|
+
(find_person("Jack").lift_to_a +
|
78
|
+
find_person("John").lift_to_a).right.map { |*names| names.join(" and ") }
|
73
79
|
# => Right("Jack and John")
|
74
80
|
|
75
|
-
#
|
81
|
+
# but if there were errors, we still have a Left with all the errors inside:
|
76
82
|
(find_person("Jack").lift_to_a +
|
77
83
|
find_person("John").lift_to_a +
|
78
84
|
find_person("Jill").lift_to_a +
|
79
85
|
find_person("Joan").lift_to_a).right.map { |*names| names.join(" and ") }
|
80
|
-
# => Left("No such person: Jill", "No such person: Joan")
|
86
|
+
# => Left(["No such person: Jill", "No such person: Joan"])
|
87
|
+
|
88
|
+
# equivalent to the previous example, but shorter:
|
89
|
+
%w(Jack John Jill Joan).map { |nm| find_person(nm).lift_to_a }.inject(:+).
|
90
|
+
right.map { |*names| names.join(" and ") }
|
91
|
+
# => Left(["No such person: Jill", "No such person: Joan"])
|
92
|
+
|
93
|
+
---
|
94
|
+
=== {Rumonade::Hash Hash}:
|
95
|
+
|
96
|
+
h = { "Foo" => 1, "Bar" => 2, "Baz" => 3 }
|
97
|
+
h = h.flat_map { |k, v| { k => v, k.upcase => v * 10 } }
|
98
|
+
# => {"Foo"=>1, "FOO"=>10, "Bar"=>2, "BAR"=>20, "Baz"=>3, "BAZ"=>30}
|
99
|
+
h = h.select { |k, v| k =~ /^b/i }
|
100
|
+
# => {"Bar"=>2, "BAR"=>20, "Baz"=>3, "BAZ"=>30}
|
101
|
+
h.get("Bar")
|
102
|
+
# => Some(2)
|
103
|
+
h.get("Foo")
|
104
|
+
# => None
|
81
105
|
|
82
106
|
(_more_ _examples_ _coming_ _soon_...)
|
83
107
|
|
@@ -96,11 +120,14 @@ The priorities for Rumonade are:
|
|
96
120
|
* <b>don't</b> mess up normal idioms of the language (e.g., Hash#map)
|
97
121
|
* <b>don't</b> slow down normal idioms of the language (e.g., Array#map)
|
98
122
|
2. Rubyish-ness of usage
|
99
|
-
* Monad is a mix-in, requiring methods self.unit and
|
100
|
-
* Prefer blocks to lambda/Procs where possible
|
123
|
+
* Monad is a mix-in, requiring methods +self.unit+ and +#bind+ be implemented by target classes
|
124
|
+
* Prefer blocks to lambda/Procs where possible, but allow both
|
101
125
|
3. Equivalent idioms to Scala where possible
|
102
126
|
|
103
127
|
== Status
|
104
128
|
|
105
|
-
Option, Either, and
|
106
|
-
|
129
|
+
Option, Either, Array, and Hash are already usable.
|
130
|
+
|
131
|
+
<b><em>Supported Ruby versions</em></b>: MRI 1.9.2, MRI 1.9.3, JRuby in 1.9 mode, and Rubinius in 1.9 mode.
|
132
|
+
|
133
|
+
Please try it out, and let me know what you think! Suggestions are always welcome.
|
data/lib/rumonade.rb
CHANGED
data/lib/rumonade/array.rb
CHANGED
@@ -14,6 +14,9 @@ module Rumonade
|
|
14
14
|
end
|
15
15
|
|
16
16
|
module InstanceMethods
|
17
|
+
# Preserve native +map+ method for performance
|
18
|
+
METHODS_TO_REPLACE_WITH_MONAD = Monad::DEFAULT_METHODS_TO_REPLACE_WITH_MONAD - [:map]
|
19
|
+
|
17
20
|
def bind(lam = nil, &blk)
|
18
21
|
inject(self.class.empty) { |arr, elt| arr + (lam || blk).call(elt).to_a }
|
19
22
|
end
|
data/lib/rumonade/either.rb
CHANGED
@@ -5,6 +5,10 @@ module Rumonade
|
|
5
5
|
# The data constructors {Rumonade::Left} and {Rumonade::Right} represent the two possible values.
|
6
6
|
# The +Either+ type is often used as an alternative to {Rumonade::Option} where {Rumonade::Left} represents
|
7
7
|
# failure (by convention) and {Rumonade::Right} is akin to {Rumonade::Some}.
|
8
|
+
#
|
9
|
+
# This implementation of +Either+ also contains ideas from the +Validation+ class in the
|
10
|
+
# +scalaz+ library.
|
11
|
+
#
|
8
12
|
# @abstract
|
9
13
|
class Either
|
10
14
|
def initialize
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'rumonade/monad'
|
2
|
+
|
3
|
+
module Rumonade
|
4
|
+
# TODO: Document use of Hash as a Monad
|
5
|
+
module HashExtensions
|
6
|
+
module ClassMethods
|
7
|
+
def unit(value)
|
8
|
+
raise ArgumentError, "argument not a 2-element Array for Hash.unit" unless (value.is_a?(Array) && value.size == 2)
|
9
|
+
Hash[*value]
|
10
|
+
end
|
11
|
+
|
12
|
+
def empty
|
13
|
+
{}
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
module InstanceMethods
|
18
|
+
# Preserve native +map+ and +flatten+ methods for compatibility
|
19
|
+
METHODS_TO_REPLACE_WITH_MONAD = Monad::DEFAULT_METHODS_TO_REPLACE_WITH_MONAD - [:map, :flatten]
|
20
|
+
|
21
|
+
def bind(lam = nil, &blk)
|
22
|
+
inject(self.class.empty) { |hsh, elt| hsh.merge((lam || blk).call(elt)) }
|
23
|
+
end
|
24
|
+
|
25
|
+
# @return [Option] a Some containing the value associated with +key+, or None if not present
|
26
|
+
def get(key)
|
27
|
+
Option(self[key])
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
Hash.send(:extend, Rumonade::HashExtensions::ClassMethods)
|
34
|
+
Hash.send(:include, Rumonade::HashExtensions::InstanceMethods)
|
35
|
+
Hash.send(:include, Rumonade::Monad)
|
data/lib/rumonade/monad.rb
CHANGED
@@ -1,19 +1,25 @@
|
|
1
1
|
module Rumonade
|
2
|
-
# Mix-in for common monad functionality dependent on implementation of monadic methods unit and bind
|
2
|
+
# Mix-in for common monad functionality dependent on implementation of monadic methods +unit+ and +bind+
|
3
3
|
#
|
4
4
|
# Notes:
|
5
|
-
# *
|
5
|
+
# * Classes should include this module AFTER defining the monadic methods +unit+ and +bind+
|
6
|
+
# * When +Monad+ is mixed into a class, if the class already contains methods in
|
7
|
+
# {METHODS_TO_REPLACE}, they will be renamed to add the suffix +_without_monad+,
|
8
|
+
# and replaced with the method defined here which has the suffix +_with_monad+
|
6
9
|
#
|
7
10
|
module Monad
|
8
|
-
|
11
|
+
# Methods to replace when mixed in -- unless class defines +METHODS_TO_REPLACE_WITH_MONAD+
|
12
|
+
DEFAULT_METHODS_TO_REPLACE_WITH_MONAD = [:map, :flat_map, :flatten]
|
13
|
+
|
14
|
+
# When mixed into a class, this callback is executed
|
15
|
+
def self.included(base)
|
16
|
+
methods_to_replace = base::METHODS_TO_REPLACE_WITH_MONAD rescue DEFAULT_METHODS_TO_REPLACE_WITH_MONAD
|
9
17
|
|
10
|
-
def self.included(base) # :nodoc:
|
11
18
|
base.class_eval do
|
12
19
|
# optimization: replace flat_map with an alias for bind, as they are identical
|
13
20
|
alias_method :flat_map_with_monad, :bind
|
14
21
|
|
15
|
-
|
16
|
-
METHODS_TO_REPLACE.each do |method_name|
|
22
|
+
methods_to_replace.each do |method_name|
|
17
23
|
alias_method "#{method_name}_without_monad".to_sym, method_name if public_instance_methods.include? method_name
|
18
24
|
alias_method method_name, "#{method_name}_with_monad".to_sym
|
19
25
|
end
|
@@ -28,19 +34,26 @@ module Rumonade
|
|
28
34
|
end
|
29
35
|
|
30
36
|
# Returns a monad whose elements are the results of applying the given function to each element in this monad
|
31
|
-
|
37
|
+
#
|
38
|
+
# NOTE: normally aliased as +map+ when +Monad+ is mixed into a class
|
39
|
+
def map_with_monad(lam = nil, &blk)
|
32
40
|
bind { |v| self.class.unit((lam || blk).call(v)) }
|
33
41
|
end
|
34
42
|
|
35
43
|
# Returns the results of applying the given function to each element in this monad
|
44
|
+
#
|
45
|
+
# NOTE: normally aliased as +flat_map+ when +Monad+ is mixed into a class
|
36
46
|
def flat_map_with_monad(lam = nil, &blk)
|
37
47
|
bind(lam || blk)
|
38
48
|
end
|
39
49
|
|
40
50
|
# Returns a monad whose elements are the ultimate (non-monadic) values contained in all nested monads
|
41
51
|
#
|
42
|
-
#
|
43
|
-
#
|
52
|
+
# NOTE: normally aliased as +flatten+ when +Monad+ is mixed into a class
|
53
|
+
#
|
54
|
+
# @example
|
55
|
+
# [Some(Some(1)), Some(Some(None))], [None]].flatten
|
56
|
+
# #=> [1]
|
44
57
|
#
|
45
58
|
def flatten_with_monad
|
46
59
|
bind { |x| x.is_a?(Monad) ? x.flatten_with_monad : self.class.unit(x) }
|
@@ -57,6 +70,16 @@ module Rumonade
|
|
57
70
|
# This method is equivalent to the Scala flatten call (single-level flattening), whereas #flatten is in keeping
|
58
71
|
# with the native Ruby flatten calls (multiple-level flattening).
|
59
72
|
#
|
73
|
+
# @example
|
74
|
+
# [Some(Some(1)), Some(Some(None))], [None]].shallow_flatten
|
75
|
+
# #=> [Some(Some(1)), Some(Some(None)), None]
|
76
|
+
# [Some(Some(1)), Some(Some(None)), None].shallow_flatten
|
77
|
+
# #=> [Some(1), Some(None)]
|
78
|
+
# [Some(1), Some(None)].shallow_flatten
|
79
|
+
# #=> [1, None]
|
80
|
+
# [1, None].shallow_flatten
|
81
|
+
# #=> [1]
|
82
|
+
#
|
60
83
|
def shallow_flatten
|
61
84
|
bind { |x| x.is_a?(Monad) ? x : self.class.unit(x) }
|
62
85
|
end
|
data/lib/rumonade/version.rb
CHANGED
data/test/hash_test.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/test_helper')
|
2
|
+
|
3
|
+
class HashTest < Test::Unit::TestCase
|
4
|
+
include Rumonade
|
5
|
+
include MonadAxiomTestHelpers
|
6
|
+
|
7
|
+
def test_when_unit_with_2_elt_array_returns_hash
|
8
|
+
assert_equal({ :k => :v }, Hash.unit([:k, :v]))
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_when_unit_with_non_2_elt_array_raises
|
12
|
+
assert_raise(ArgumentError) { Hash.unit([1]) }
|
13
|
+
assert_raise(ArgumentError) { Hash.unit([1, 2, 3]) }
|
14
|
+
assert_raise(ArgumentError) { Hash.unit([1, 2, 3, 4]) }
|
15
|
+
assert_raise(ArgumentError) { Hash.unit("not an array at all") }
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_when_empty_returns_empty_array
|
19
|
+
assert_equal({}, Hash.empty)
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_monad_axioms
|
23
|
+
f = lambda { |p| Hash.unit([p[0].downcase, p[1] * 2]) }
|
24
|
+
g = lambda { |p| Hash.unit([p[0].upcase, p[1] * 5]) }
|
25
|
+
[["Foo", 1], ["Bar", 2]].each do |value|
|
26
|
+
assert_monad_axiom_1(Hash, value, f)
|
27
|
+
assert_monad_axiom_2(Hash.unit(value))
|
28
|
+
assert_monad_axiom_3(Hash.unit(value), f, g)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_flat_map_behaves_correctly
|
33
|
+
assert_equal({ "FOO" => 2, "BAR" => 4 }, { "Foo" => 1, "Bar" => 2 }.flat_map { |p| { p[0].upcase => p[1] * 2 } })
|
34
|
+
end
|
35
|
+
|
36
|
+
# Special case: because Hash#map is built into Ruby, must preserve existing behavior
|
37
|
+
def test_map_still_behaves_normally_returning_array_of_arrays
|
38
|
+
assert_equal([["FOO", 2], ["BAR", 4]], { "Foo" => 1, "Bar" => 2 }.map { |p| [p[0].upcase, p[1] * 2] })
|
39
|
+
end
|
40
|
+
|
41
|
+
# We add Hash#map_with_monad to return a Hash, as one might expect Hash#map to do
|
42
|
+
def test_map_with_monad_behaves_correctly_returning_hash
|
43
|
+
assert_equal({ "FOO" => 2, "BAR" => 4 }, { "Foo" => 1, "Bar" => 2 }.map_with_monad { |p| [p[0].upcase, p[1] * 2] })
|
44
|
+
end
|
45
|
+
|
46
|
+
# Special case: because Hash#flatten is built into Ruby, must preserve existing behavior
|
47
|
+
unless (RUBY_ENGINE.to_s rescue nil) == 'rbx' # Except on Rubinius, where it's broken, apparently
|
48
|
+
def test_flatten_still_behaves_normaly_returning_array_of_alternating_keys_and_values
|
49
|
+
assert_equal ["Foo", 1, "Bar", 2], { "Foo" => 1, "Bar" => 2 }.flatten
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_shallow_flatten_raises_type_error
|
54
|
+
assert_raise(TypeError) { { "Foo" => "Bar" }.shallow_flatten }
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_get_returns_option_of_value
|
58
|
+
assert_equal None, { "Foo" => 1, "Bar" => 2 }.get("Baz")
|
59
|
+
assert_equal Some(1), { "Foo" => 1, "Bar" => 2 }.get("Foo")
|
60
|
+
end
|
61
|
+
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.
|
4
|
+
version: 0.4.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-
|
12
|
+
date: 2012-11-12 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: test-unit
|
@@ -63,6 +63,7 @@ files:
|
|
63
63
|
- lib/rumonade/either.rb
|
64
64
|
- lib/rumonade/error_handling.rb
|
65
65
|
- lib/rumonade/errors.rb
|
66
|
+
- lib/rumonade/hash.rb
|
66
67
|
- lib/rumonade/lazy_identity.rb
|
67
68
|
- lib/rumonade/monad.rb
|
68
69
|
- lib/rumonade/option.rb
|
@@ -71,6 +72,7 @@ files:
|
|
71
72
|
- test/array_test.rb
|
72
73
|
- test/either_test.rb
|
73
74
|
- test/error_handling_test.rb
|
75
|
+
- test/hash_test.rb
|
74
76
|
- test/lazy_identity_test.rb
|
75
77
|
- test/option_test.rb
|
76
78
|
- test/test_helper.rb
|
@@ -94,7 +96,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
94
96
|
version: '0'
|
95
97
|
requirements: []
|
96
98
|
rubyforge_project: rumonade
|
97
|
-
rubygems_version: 1.8.
|
99
|
+
rubygems_version: 1.8.24
|
98
100
|
signing_key:
|
99
101
|
specification_version: 3
|
100
102
|
summary: A Scala-inspired Monad library for Ruby
|
@@ -102,7 +104,7 @@ test_files:
|
|
102
104
|
- test/array_test.rb
|
103
105
|
- test/either_test.rb
|
104
106
|
- test/error_handling_test.rb
|
107
|
+
- test/hash_test.rb
|
105
108
|
- test/lazy_identity_test.rb
|
106
109
|
- test/option_test.rb
|
107
110
|
- test/test_helper.rb
|
108
|
-
has_rdoc:
|