rumonade 0.4.0 → 0.4.1
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/.rvmrc +48 -0
- data/HISTORY.md +6 -0
- data/MIT-LICENSE.txt +1 -1
- data/README.md +148 -0
- data/lib/rumonade/array.rb +4 -1
- data/lib/rumonade/either.rb +3 -0
- data/lib/rumonade/monad.rb +9 -7
- data/lib/rumonade/version.rb +1 -1
- data/test/array_test.rb +13 -0
- metadata +5 -4
- data/README.rdoc +0 -133
data/.rvmrc
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
#!/usr/bin/env bash
|
2
|
+
|
3
|
+
# This is an RVM Project .rvmrc file, used to automatically load the ruby
|
4
|
+
# development environment upon cd'ing into the directory
|
5
|
+
|
6
|
+
# First we specify our desired <ruby>[@<gemset>], the @gemset name is optional,
|
7
|
+
# Only full ruby name is supported here, for short names use:
|
8
|
+
# echo "rvm use 1.9.3" > .rvmrc
|
9
|
+
environment_id="ruby-1.9.3-p385@rumonade"
|
10
|
+
|
11
|
+
# Uncomment the following lines if you want to verify rvm version per project
|
12
|
+
# rvmrc_rvm_version="1.18.8 (stable)" # 1.10.1 seams as a safe start
|
13
|
+
# eval "$(echo ${rvm_version}.${rvmrc_rvm_version} | awk -F. '{print "[[ "$1*65536+$2*256+$3" -ge "$4*65536+$5*256+$6" ]]"}' )" || {
|
14
|
+
# echo "This .rvmrc file requires at least RVM ${rvmrc_rvm_version}, aborting loading."
|
15
|
+
# return 1
|
16
|
+
# }
|
17
|
+
|
18
|
+
# First we attempt to load the desired environment directly from the environment
|
19
|
+
# file. This is very fast and efficient compared to running through the entire
|
20
|
+
# CLI and selector. If you want feedback on which environment was used then
|
21
|
+
# insert the word 'use' after --create as this triggers verbose mode.
|
22
|
+
if [[ -d "${rvm_path:-$HOME/.rvm}/environments"
|
23
|
+
&& -s "${rvm_path:-$HOME/.rvm}/environments/$environment_id" ]]
|
24
|
+
then
|
25
|
+
\. "${rvm_path:-$HOME/.rvm}/environments/$environment_id"
|
26
|
+
[[ -s "${rvm_path:-$HOME/.rvm}/hooks/after_use" ]] &&
|
27
|
+
\. "${rvm_path:-$HOME/.rvm}/hooks/after_use" || true
|
28
|
+
else
|
29
|
+
# If the environment file has not yet been created, use the RVM CLI to select.
|
30
|
+
rvm --create "$environment_id" || {
|
31
|
+
echo "Failed to create RVM environment '${environment_id}'."
|
32
|
+
return 1
|
33
|
+
}
|
34
|
+
fi
|
35
|
+
|
36
|
+
# If you use bundler, this might be useful to you:
|
37
|
+
# if [[ -s Gemfile ]] && {
|
38
|
+
# ! builtin command -v bundle >/dev/null ||
|
39
|
+
# builtin command -v bundle | GREP_OPTIONS= \grep $rvm_path/bin/bundle >/dev/null
|
40
|
+
# }
|
41
|
+
# then
|
42
|
+
# printf "%b" "The rubygem 'bundler' is not installed. Installing it now.\n"
|
43
|
+
# gem install bundler
|
44
|
+
# fi
|
45
|
+
# if [[ -s Gemfile ]] && builtin command -v bundle >/dev/null
|
46
|
+
# then
|
47
|
+
# bundle install | GREP_OPTIONS= \grep -vE '^Using|Your bundle is complete'
|
48
|
+
# fi
|
data/HISTORY.md
CHANGED
@@ -1,5 +1,11 @@
|
|
1
1
|
# HISTORY
|
2
2
|
|
3
|
+
## v0.4.1 (May 9, 2013)
|
4
|
+
|
5
|
+
- fixed behavior of #flatten called with optional depth parameter
|
6
|
+
- thanks Martin Mauch (@nightscape)!
|
7
|
+
- See full list @ https://github.com/ms-ati/rumonade/compare/v0.4.0...v0.4.1
|
8
|
+
|
3
9
|
## v0.4.0 (Nov 11, 2012)
|
4
10
|
|
5
11
|
- added scala-like extensions to Hash
|
data/MIT-LICENSE.txt
CHANGED
data/README.md
ADDED
@@ -0,0 +1,148 @@
|
|
1
|
+
# [Rumonade](https://rubygems.org/gems/rumonade)
|
2
|
+
|
3
|
+
[](https://travis-ci.org/ms-ati/rumonade)
|
4
|
+
[](https://gemnasium.com/ms-ati/rumonade)
|
5
|
+
|
6
|
+
*_Project_*: [github](http://github.com/ms-ati/rumonade)
|
7
|
+
|
8
|
+
*_Documentation_*: [rubydoc.info](http://rubydoc.info/gems/rumonade/frames)
|
9
|
+
|
10
|
+
## 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)
|
11
|
+
|
12
|
+
Are you working in both the [Scala](http://www.scala-lang.org) and [Ruby](http://www.ruby-lang.org) worlds,
|
13
|
+
and finding that you miss some of the practical benefits of Scala's
|
14
|
+
[monads](http://james-iry.blogspot.com/2007/09/monads-are-elephants-part-1.html) in Ruby?
|
15
|
+
If so, then Rumonade is for you.
|
16
|
+
|
17
|
+
The goal of this library is to make the most common and useful Scala monadic idioms available in Ruby via the following classes:
|
18
|
+
* [Option](http://rubydoc.info/gems/rumonade/Rumonade/Option)
|
19
|
+
* [Array](http://rubydoc.info/gems/rumonade/Rumonade/ArrayExtensions)
|
20
|
+
* [Either](http://rubydoc.info/gems/rumonade/Rumonade/Either)
|
21
|
+
* [Hash](http://rubydoc.info/gems/rumonade/Rumonade/Hash)
|
22
|
+
* _more coming soon_
|
23
|
+
|
24
|
+
Syntactic support for scala-like [for-comprehensions](http://www.scala-lang.org/node/111) will be implemented
|
25
|
+
as a sequence of calls to `flat_map`, `select`, etc, modeling [Scala's
|
26
|
+
approach](http://stackoverflow.com/questions/3754089/scala-for-comprehension/3754568#3754568).
|
27
|
+
|
28
|
+
Support for an [all_catch](http://www.scala-lang.org/archives/downloads/distrib/files/nightly/docs/library/scala/util/control/Exception$.html)
|
29
|
+
idiom will be implemented to turn blocks which might throw exceptions into Option or Either
|
30
|
+
results. If this proves useful (and a good fit for Ruby), then more narrow functional catchers can be implemented as well.
|
31
|
+
|
32
|
+
## Usage Examples
|
33
|
+
|
34
|
+
### Option: handle _possibly_ _nil_ values in a _functional_ fashion:
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
def format_date_in_march(time_or_date_or_nil)
|
38
|
+
Option(time_or_date_or_nil). # wraps possibly-nil value in an Option monad (Some or None)
|
39
|
+
map(&:to_date). # transforms a contained Time value into a Date value
|
40
|
+
select {|d| d.month == 3}. # filters out non-matching Date values (Some becomes None)
|
41
|
+
map(&:to_s). # transforms a contained Date value into a String value
|
42
|
+
map {|s| s.gsub('-', '')}. # transforms a contained String value by removing '-'
|
43
|
+
get_or_else("not in march!") # returns the contained value, or the alternative if None
|
44
|
+
end
|
45
|
+
|
46
|
+
format_date_in_march(nil) # => "not in march!"
|
47
|
+
format_date_in_march(Time.parse('2009-01-01 01:02')) # => "not in march!"
|
48
|
+
format_date_in_march(Time.parse('2011-03-21 12:34')) # => "20110321"
|
49
|
+
```
|
50
|
+
|
51
|
+
Note:
|
52
|
+
* each step of the chained computations above are functionally isolated
|
53
|
+
* the value can notionally _start_ as nil, or _become_ nil during a computation, without effecting any other chained computations
|
54
|
+
|
55
|
+
---
|
56
|
+
### Either: handle failures (Left) and successes (Right) in a _functional_ fashion:
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
def find_person(name)
|
60
|
+
case name
|
61
|
+
when /Jack/i, /John/i
|
62
|
+
Right(name.capitalize)
|
63
|
+
else
|
64
|
+
Left("No such person: #{name.capitalize}")
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# success looks like this:
|
69
|
+
find_person("Jack")
|
70
|
+
# => Right("Jack")
|
71
|
+
|
72
|
+
# failure looks like this:
|
73
|
+
find_person("Jill")
|
74
|
+
# => Left("No such person: Jill")
|
75
|
+
|
76
|
+
# lift the contained values into Array, in order to combine them:
|
77
|
+
find_person("Joan").lift_to_a
|
78
|
+
# => Left(["No such person: Joan"])
|
79
|
+
|
80
|
+
# on the 'happy path', combine and transform successes into a single success result:
|
81
|
+
(find_person("Jack").lift_to_a +
|
82
|
+
find_person("John").lift_to_a).right.map { |*names| names.join(" and ") }
|
83
|
+
# => Right("Jack and John")
|
84
|
+
|
85
|
+
# but if there were errors, we still have a Left with all the errors inside:
|
86
|
+
(find_person("Jack").lift_to_a +
|
87
|
+
find_person("John").lift_to_a +
|
88
|
+
find_person("Jill").lift_to_a +
|
89
|
+
find_person("Joan").lift_to_a).right.map { |*names| names.join(" and ") }
|
90
|
+
# => Left(["No such person: Jill", "No such person: Joan"])
|
91
|
+
|
92
|
+
# equivalent to the previous example, but shorter:
|
93
|
+
%w(Jack John Jill Joan).
|
94
|
+
map { |nm| find_person(nm).lift_to_a }.inject(:+).
|
95
|
+
right.map { |*names| names.join(" and ") }
|
96
|
+
# => Left(["No such person: Jill", "No such person: Joan"])
|
97
|
+
```
|
98
|
+
|
99
|
+
Also, see the `Either` class in action in the [Ruby version](https://gist.github.com/2553490)
|
100
|
+
of [A Tale of Three Nightclubs](http://bugsquash.blogspot.com/2012/03/example-of-applicative-validation-in.html)
|
101
|
+
validation example in F#, and compare it to the [Scala version using scalaz](https://gist.github.com/970717).
|
102
|
+
|
103
|
+
---
|
104
|
+
### Hash: `flat_map` returns a Hash for each key/value pair; `get` returns an Option
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
h = { "Foo" => 1, "Bar" => 2, "Baz" => 3 }
|
108
|
+
|
109
|
+
h = h.flat_map { |k, v| { k => v, k.upcase => v * 10 } }
|
110
|
+
# => {"Foo"=>1, "FOO"=>10, "Bar"=>2, "BAR"=>20, "Baz"=>3, "BAZ"=>30}
|
111
|
+
|
112
|
+
h = h.select { |k, v| k =~ /^b/i }
|
113
|
+
# => {"Bar"=>2, "BAR"=>20, "Baz"=>3, "BAZ"=>30}
|
114
|
+
|
115
|
+
h.get("Bar")
|
116
|
+
# => Some(2)
|
117
|
+
|
118
|
+
h.get("Foo")
|
119
|
+
# => None
|
120
|
+
```
|
121
|
+
|
122
|
+
## Approach
|
123
|
+
|
124
|
+
There have been [many](http://moonbase.rydia.net/mental/writings/programming/monads-in-ruby/00introduction.html)
|
125
|
+
[posts](http://pretheory.wordpress.com/2008/02/14/the-maybe-monad-in-ruby/)
|
126
|
+
[and](http://www.valuedlessons.com/2008/01/monads-in-ruby-with-nice-syntax.html)
|
127
|
+
[discussions](http://stackoverflow.com/questions/2709361/monad-equivalent-in-ruby)
|
128
|
+
about monads in Ruby, which have sparked a number of approaches.
|
129
|
+
|
130
|
+
Rumonade wants to be a practical drop-in Monad solution that will fit well into the Ruby world.
|
131
|
+
|
132
|
+
The priorities for Rumonade are:
|
133
|
+
|
134
|
+
1. Practical usability in day-to-day Ruby
|
135
|
+
* <b>don't</b> mess up normal idioms of the language (e.g., `Hash#map`)
|
136
|
+
* <b>don't</b> slow down normal idioms of the language (e.g., `Array#map`)
|
137
|
+
2. Rubyish-ness of usage
|
138
|
+
* Monad is a mix-in, requiring methods `self.unit` and `#bind` be implemented by target class
|
139
|
+
* Prefer blocks to lambda/Procs where possible, but allow both
|
140
|
+
3. Equivalent idioms to Scala where possible
|
141
|
+
|
142
|
+
## Status
|
143
|
+
|
144
|
+
Option, Either, Array, and Hash are already usable.
|
145
|
+
|
146
|
+
<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.
|
147
|
+
|
148
|
+
Please try it out, and let me know what you think! Suggestions are always welcome.
|
data/lib/rumonade/array.rb
CHANGED
@@ -18,7 +18,10 @@ module Rumonade
|
|
18
18
|
METHODS_TO_REPLACE_WITH_MONAD = Monad::DEFAULT_METHODS_TO_REPLACE_WITH_MONAD - [:map]
|
19
19
|
|
20
20
|
def bind(lam = nil, &blk)
|
21
|
-
inject(self.class.empty)
|
21
|
+
inject(self.class.empty) do |arr, elt|
|
22
|
+
res = (lam || blk).call(elt)
|
23
|
+
arr + (res.respond_to?(:to_a) ? res.to_a : [res])
|
24
|
+
end
|
22
25
|
end
|
23
26
|
end
|
24
27
|
end
|
data/lib/rumonade/either.rb
CHANGED
data/lib/rumonade/monad.rb
CHANGED
@@ -55,8 +55,12 @@ module Rumonade
|
|
55
55
|
# [Some(Some(1)), Some(Some(None))], [None]].flatten
|
56
56
|
# #=> [1]
|
57
57
|
#
|
58
|
-
def flatten_with_monad
|
59
|
-
|
58
|
+
def flatten_with_monad(depth=nil)
|
59
|
+
if depth.is_a? Integer
|
60
|
+
depth.times.inject(self) {|e, _| e.shallow_flatten }
|
61
|
+
else
|
62
|
+
bind { |x| x.is_a?(Monad) ? x.flatten_with_monad : self.class.unit(x) }
|
63
|
+
end
|
60
64
|
end
|
61
65
|
|
62
66
|
# Returns a monad whose elements are all those elements of this monad for which the given predicate returned true
|
@@ -71,11 +75,9 @@ module Rumonade
|
|
71
75
|
# with the native Ruby flatten calls (multiple-level flattening).
|
72
76
|
#
|
73
77
|
# @example
|
74
|
-
# [Some(Some(1)), Some(Some(None))
|
75
|
-
# #=> [Some(
|
76
|
-
# [Some(
|
77
|
-
# #=> [Some(1), Some(None)]
|
78
|
-
# [Some(1), Some(None)].shallow_flatten
|
78
|
+
# [Some(Some(1)), Some(Some(None)), [None]].shallow_flatten
|
79
|
+
# #=> [Some(1), Some(None), None]
|
80
|
+
# [Some(1), Some(None), None].shallow_flatten
|
79
81
|
# #=> [1, None]
|
80
82
|
# [1, None].shallow_flatten
|
81
83
|
# #=> [1]
|
data/lib/rumonade/version.rb
CHANGED
data/test/array_test.rb
CHANGED
@@ -24,6 +24,7 @@ class ArrayTest < Test::Unit::TestCase
|
|
24
24
|
|
25
25
|
def test_flat_map_behaves_correctly
|
26
26
|
assert_equal ["FOO", "BAR"], ["foo", "bar"].flat_map { |s| [s.upcase] }
|
27
|
+
assert_equal [2, 4, 6], [1, 2, 3].flat_map { |i| i * 2 }
|
27
28
|
end
|
28
29
|
|
29
30
|
def test_map_behaves_correctly
|
@@ -35,6 +36,7 @@ class ArrayTest < Test::Unit::TestCase
|
|
35
36
|
assert_equal [1], [None, Some(1)].shallow_flatten
|
36
37
|
assert_equal [1, Some(2)], [None, Some(1), Some(Some(2))].shallow_flatten
|
37
38
|
assert_equal [Some(Some(None))], [Some(Some(Some(None)))].shallow_flatten
|
39
|
+
assert_equal [Some(1), Some(None), None], [Some(Some(1)), Some(Some(None)), [None]].shallow_flatten
|
38
40
|
end
|
39
41
|
|
40
42
|
def test_flatten_behaves_correctly
|
@@ -42,4 +44,15 @@ class ArrayTest < Test::Unit::TestCase
|
|
42
44
|
assert_equal [1, 2], [None, Some(1), Some(Some(2))].flatten
|
43
45
|
assert_equal [], [Some(Some(Some(None)))].flatten
|
44
46
|
end
|
47
|
+
|
48
|
+
def test_flatten_with_argument_behaves_correctly
|
49
|
+
assert_equal [0, 1, [2], [[3]], [[[4]]]], [0, [1], [[2]], [[[3]]], [[[[4]]]]].flatten(1)
|
50
|
+
assert_equal [0, 1, 2, [3], [[4]]], [0, [1], [[2]], [[[3]]], [[[[4]]]]].flatten(2)
|
51
|
+
assert_equal [0, 1, 2, 3, [4]], [0, [1], [[2]], [[[3]]], [[[[4]]]]].flatten(3)
|
52
|
+
assert_equal [0, 1, 2, 3, 4], [0, [1], [[2]], [[[3]]], [[[[4]]]]].flatten(4)
|
53
|
+
assert_equal [Some(Some(1)), Some(Some(None)), [None]], [Some(Some(1)), Some(Some(None)), [None]].flatten(0)
|
54
|
+
assert_equal [Some(1), Some(None), None], [Some(Some(1)), Some(Some(None)), [None]].flatten(1)
|
55
|
+
assert_equal [1, None], [Some(Some(1)), Some(Some(None)), [None]].flatten(2)
|
56
|
+
assert_equal [1], [Some(Some(1)), Some(Some(None)), [None]].flatten(3)
|
57
|
+
end
|
45
58
|
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.
|
4
|
+
version: 0.4.1
|
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:
|
12
|
+
date: 2013-05-09 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: test-unit
|
@@ -52,11 +52,12 @@ extensions: []
|
|
52
52
|
extra_rdoc_files: []
|
53
53
|
files:
|
54
54
|
- .gitignore
|
55
|
+
- .rvmrc
|
55
56
|
- .travis.yml
|
56
57
|
- Gemfile
|
57
58
|
- HISTORY.md
|
58
59
|
- MIT-LICENSE.txt
|
59
|
-
- README.
|
60
|
+
- README.md
|
60
61
|
- Rakefile
|
61
62
|
- lib/rumonade.rb
|
62
63
|
- lib/rumonade/array.rb
|
@@ -96,7 +97,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
96
97
|
version: '0'
|
97
98
|
requirements: []
|
98
99
|
rubyforge_project: rumonade
|
99
|
-
rubygems_version: 1.8.
|
100
|
+
rubygems_version: 1.8.25
|
100
101
|
signing_key:
|
101
102
|
specification_version: 3
|
102
103
|
summary: A Scala-inspired Monad library for Ruby
|
data/README.rdoc
DELETED
@@ -1,133 +0,0 @@
|
|
1
|
-
{<img src="https://secure.travis-ci.org/ms-ati/rumonade.png?branch=master" alt="Build Status" />}[http://travis-ci.org/ms-ati/rumonade]
|
2
|
-
|
3
|
-
= Rumonade[https://rubygems.org/gems/rumonade]
|
4
|
-
|
5
|
-
*_Project_*: github[http://github.com/ms-ati/rumonade]
|
6
|
-
|
7
|
-
*_Documentation_*: rubydoc.info[http://rubydoc.info/gems/rumonade/frames]
|
8
|
-
|
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
|
-
|
11
|
-
Are you working in both the Scala[http://www.scala-lang.org] and Ruby[http://www.ruby-lang.org] worlds,
|
12
|
-
and finding that you miss some of the practical benefits of Scala's
|
13
|
-
monads[http://james-iry.blogspot.com/2007/09/monads-are-elephants-part-1.html] in Ruby?
|
14
|
-
Then Rumonade is for you.
|
15
|
-
|
16
|
-
The goal of this library is to make the most common and useful Scala monadic idioms available in Ruby via the following classes:
|
17
|
-
* {Rumonade::Option Option}
|
18
|
-
* {Rumonade::ArrayExtensions Array}
|
19
|
-
* {Rumonade::Either Either}
|
20
|
-
* {Rumonade::Hash Hash}
|
21
|
-
* (_more_ _TBD_)
|
22
|
-
|
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, modeling Scala's
|
25
|
-
approach[http://stackoverflow.com/questions/3754089/scala-for-comprehension/3754568#3754568].
|
26
|
-
|
27
|
-
Support for an all_catch[http://www.scala-lang.org/archives/downloads/distrib/files/nightly/docs/library/scala/util/control/Exception$.html]
|
28
|
-
idiom will be implemented to turn blocks which might throw exceptions into Option or Either
|
29
|
-
results. If this proves useful (and a good fit for Ruby), then more narrow functional catchers can be implemented as well.
|
30
|
-
|
31
|
-
== Usage Examples
|
32
|
-
|
33
|
-
=== {Rumonade::Option Option}: handle _possibly_ _nil_ values in a _functional_ fashion:
|
34
|
-
|
35
|
-
def format_date_in_march(time_or_date_or_nil)
|
36
|
-
Option(time_or_date_or_nil). # wraps possibly-nil value in an Option monad (Some or None)
|
37
|
-
map(&:to_date). # transforms a contained Time value into a Date value
|
38
|
-
select {|d| d.month == 3}. # filters out non-matching Date values (Some becomes None)
|
39
|
-
map(&:to_s). # transforms a contained Date value into a String value
|
40
|
-
map {|s| s.gsub('-', '')}. # transforms a contained String value by removing '-'
|
41
|
-
get_or_else("not in march!") # returns the contained value, or the alternative if None
|
42
|
-
end
|
43
|
-
|
44
|
-
format_date_in_march(nil) # => "not in march!"
|
45
|
-
format_date_in_march(Time.parse('2009-01-01 01:02')) # => "not in march!"
|
46
|
-
format_date_in_march(Time.parse('2011-03-21 12:34')) # => "20110321"
|
47
|
-
|
48
|
-
Note:
|
49
|
-
* each step of the chained computations above are functionally isolated
|
50
|
-
* the value can notionally _start_ as nil, or _become_ nil during a computation, without effecting any other chained computations
|
51
|
-
|
52
|
-
---
|
53
|
-
=== {Rumonade::Either Either}: handle failures ({Rumonade::Left Left}) and successes ({Rumonade::Right Right}) in a _functional_ fashion:
|
54
|
-
|
55
|
-
def find_person(name)
|
56
|
-
case name
|
57
|
-
when /Jack/i, /John/i
|
58
|
-
Right(name.capitalize)
|
59
|
-
else
|
60
|
-
Left("No such person: #{name.capitalize}")
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
# success looks like this:
|
65
|
-
find_person("Jack")
|
66
|
-
# => Right("Jack")
|
67
|
-
|
68
|
-
# failure looks like this:
|
69
|
-
find_person("Jill")
|
70
|
-
# => Left("No such person: Jill")
|
71
|
-
|
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 ") }
|
79
|
-
# => Right("Jack and John")
|
80
|
-
|
81
|
-
# but if there were errors, we still have a Left with all the errors inside:
|
82
|
-
(find_person("Jack").lift_to_a +
|
83
|
-
find_person("John").lift_to_a +
|
84
|
-
find_person("Jill").lift_to_a +
|
85
|
-
find_person("Joan").lift_to_a).right.map { |*names| names.join(" and ") }
|
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
|
105
|
-
|
106
|
-
(_more_ _examples_ _coming_ _soon_...)
|
107
|
-
|
108
|
-
== Approach
|
109
|
-
|
110
|
-
There have been many[http://moonbase.rydia.net/mental/writings/programming/monads-in-ruby/00introduction.html]
|
111
|
-
posts[http://pretheory.wordpress.com/2008/02/14/the-maybe-monad-in-ruby/]
|
112
|
-
and[http://www.valuedlessons.com/2008/01/monads-in-ruby-with-nice-syntax.html]
|
113
|
-
discussions[http://stackoverflow.com/questions/2709361/monad-equivalent-in-ruby]
|
114
|
-
about monads in Ruby, which have sparked a number of approaches.
|
115
|
-
|
116
|
-
Rumonade wants to be a practical drop-in Monad solution that will fit well into the Ruby world.
|
117
|
-
|
118
|
-
The priorities for Rumonade are:
|
119
|
-
1. Practical usability in day-to-day Ruby
|
120
|
-
* <b>don't</b> mess up normal idioms of the language (e.g., Hash#map)
|
121
|
-
* <b>don't</b> slow down normal idioms of the language (e.g., Array#map)
|
122
|
-
2. Rubyish-ness of usage
|
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
|
125
|
-
3. Equivalent idioms to Scala where possible
|
126
|
-
|
127
|
-
== Status
|
128
|
-
|
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.
|