rumonade 0.4.0 → 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.org/ms-ati/rumonade.png)](https://travis-ci.org/ms-ati/rumonade)
|
4
|
+
[![Dependency Status](https://gemnasium.com/ms-ati/rumonade.png)](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.
|