rumonade 0.3.0 → 0.4.0
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/.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:
|