├── .coveralls.yml
├── .gitignore
├── .ruby-gemset
├── .ruby-version
├── .travis.yml
├── Gemfile
├── HISTORY.md
├── MIT-LICENSE.txt
├── README.md
├── Rakefile
├── lib
├── rumonade.rb
└── rumonade
│ ├── array.rb
│ ├── either.rb
│ ├── error_handling.rb
│ ├── errors.rb
│ ├── hash.rb
│ ├── lazy_identity.rb
│ ├── monad.rb
│ ├── option.rb
│ └── version.rb
├── rumonade.gemspec
└── test
├── array_test.rb
├── either_test.rb
├── error_handling_test.rb
├── hash_test.rb
├── lazy_identity_test.rb
├── option_test.rb
└── test_helper.rb
/.coveralls.yml:
--------------------------------------------------------------------------------
1 | service_name: travis-ci
2 |
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | .bundle
3 | Gemfile.lock
4 | pkg/*
5 | .idea
6 | html
7 | rdoc
8 | .yardoc
9 | doc
10 | coverage
11 |
--------------------------------------------------------------------------------
/.ruby-gemset:
--------------------------------------------------------------------------------
1 | rumonade
2 |
--------------------------------------------------------------------------------
/.ruby-version:
--------------------------------------------------------------------------------
1 | ruby-2.1.2
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 | cache: bundler
3 | sudo: false
4 | rvm:
5 | - 2.1.2
6 | - 2.0.0
7 | - 1.9.2
8 | - 1.9.3
9 | - jruby-19mode
10 | - rbx-2
11 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | # Specify your gem's dependencies in rumonade.gemspec
4 | gemspec
5 |
--------------------------------------------------------------------------------
/HISTORY.md:
--------------------------------------------------------------------------------
1 | # HISTORY
2 |
3 | ## v0.4.4 (Aug 5, 2015)
4 |
5 | - Fix: don't replace Array#flatten, as it breaks Rails 3 (see #8)
6 | - See full list @ https://github.com/ms-ati/rumonade/compare/v0.4.3...v0.4.4
7 |
8 | ## v0.4.3 (Aug 27, 2013)
9 |
10 | - Fix: prevent Array#flatten from attempting to flatten Hash (thanks @moser!)
11 | - See full list @ https://github.com/ms-ati/rumonade/compare/v0.4.2...v0.4.3
12 |
13 | ## v0.4.2 (May 9, 2013)
14 |
15 | - revert change which confused Array#map with Array#flat_map
16 | - See full list @ https://github.com/ms-ati/rumonade/compare/v0.4.1...v0.4.2
17 |
18 | ## v0.4.1 (May 9, 2013)
19 |
20 | - fixed behavior of #flatten called with optional depth parameter
21 | - thanks Martin Mauch (@nightscape)!
22 | - See full list @ https://github.com/ms-ati/rumonade/compare/v0.4.0...v0.4.1
23 |
24 | ## v0.4.0 (Nov 11, 2012)
25 |
26 | - added scala-like extensions to Hash
27 | - See full list @ https://github.com/ms-ati/rumonade/compare/v0.3.0...v0.4.0
28 |
29 | ## v0.3.0 (Apr 29, 2012)
30 |
31 | - added Either#lift_to_a (and #lift)
32 | - See full list @ https://github.com/ms-ati/rumonade/compare/v0.2.2...v0.3.0
33 |
34 | ## v0.2.2 (Apr 26, 2012)
35 |
36 | - added Either#+ to allow combining validations
37 | - See full list @ https://github.com/ms-ati/rumonade/compare/v0.2.1...v0.2.2
38 |
39 | ## v0.2.1 (Oct 22, 2011)
40 |
41 | - fixed Option#map to disallow becoming None on a nil result
42 | - other fixes and documentation updates
43 | - See full list @ https://github.com/ms-ati/rumonade/compare/v0.2.0...v0.2.1
44 |
45 | ## v0.2.0 (Oct 18, 2011)
46 |
47 | - added a scala-like Either class w/ LeftProjection and RightProjection monads
48 |
49 | ## v0.1.2 (Oct 12, 2011)
50 |
51 | - progress towards Either class
52 | - changed documentation to yard from rdoc
53 |
54 | ## v0.1.1 (Sep 19, 2011)
55 |
56 | - added a first stab at documentation for Option
57 | - fixed certain errors with #map
58 | - added #select
59 |
60 | ## v0.1.0 (Sep 17, 2011)
61 |
62 | - Initial Feature Set
63 | - general implementation and testing of monadic laws based on `unit` and `bind`
64 | - scala-like Option class w/ Some & None
65 | - scala-like extensions to Array
66 |
--------------------------------------------------------------------------------
/MIT-LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2011-2015 Marc Siegel
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # NOTICE: Re-thinking is happening...
2 |
3 | Scala has evolved quite a bit since this project began in 2011, and mainstream Ruby usage
4 | has evolved as well.
5 |
6 | In the branch [experimental-0.5](https://github.com/ms-ati/rumonade/tree/experimental-0.5),
7 | new things are going to be tried.
8 |
9 | If you're interested in sharing your ideas, join the [mailing list](http://groups.google.com/group/rumonade).
10 |
11 | ---------------------------------------
12 |
13 | # [Rumonade](https://rubygems.org/gems/rumonade)
14 |
15 | [](https://rubygems.org/gems/rumonade)
16 | [](https://travis-ci.org/ms-ati/rumonade)
17 | [](https://gemnasium.com/ms-ati/rumonade)
18 | [](https://codeclimate.com/github/ms-ati/rumonade)
19 | [](https://coveralls.io/r/ms-ati/rumonade)
20 |
21 |
22 | *_Project_*: [github](http://github.com/ms-ati/rumonade)
23 |
24 | *_Documentation_*: [rubydoc.info](http://rubydoc.info/gems/rumonade/frames)
25 |
26 | ## 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)
27 |
28 | Are you working in both the [Scala](http://www.scala-lang.org) and [Ruby](http://www.ruby-lang.org) worlds,
29 | and finding that you miss some of the practical benefits of Scala's
30 | [monads](http://james-iry.blogspot.com/2007/09/monads-are-elephants-part-1.html) in Ruby?
31 | If so, then Rumonade is for you.
32 |
33 | The goal of this library is to make the most common and useful Scala monadic idioms available in Ruby via the following classes:
34 | * [Option](http://rubydoc.info/gems/rumonade/Rumonade/Option)
35 | * [Array](http://rubydoc.info/gems/rumonade/Rumonade/ArrayExtensions)
36 | * [Either](http://rubydoc.info/gems/rumonade/Rumonade/Either)
37 | * [Hash](http://rubydoc.info/gems/rumonade/Rumonade/Hash)
38 | * _more coming soon_
39 |
40 | Syntactic support for scala-like [for-comprehensions](http://www.scala-lang.org/node/111) will be implemented
41 | as a sequence of calls to `flat_map`, `select`, etc, modeling [Scala's
42 | approach](http://stackoverflow.com/questions/3754089/scala-for-comprehension/3754568#3754568).
43 |
44 | Support for an [all_catch](http://www.scala-lang.org/archives/downloads/distrib/files/nightly/docs/library/scala/util/control/Exception$.html)
45 | idiom will be implemented to turn blocks which might throw exceptions into Option or Either
46 | results. If this proves useful (and a good fit for Ruby), then more narrow functional catchers can be implemented as well.
47 |
48 | ## Usage Examples
49 |
50 | ### Option: handle _possibly_ _nil_ values in a _functional_ fashion:
51 |
52 | ```ruby
53 | def format_date_in_march(time_or_date_or_nil)
54 | Option(time_or_date_or_nil). # wraps possibly-nil value in an Option monad (Some or None)
55 | map(&:to_date). # transforms a contained Time value into a Date value
56 | select {|d| d.month == 3}. # filters out non-matching Date values (Some becomes None)
57 | map(&:to_s). # transforms a contained Date value into a String value
58 | map {|s| s.gsub('-', '')}. # transforms a contained String value by removing '-'
59 | get_or_else("not in march!") # returns the contained value, or the alternative if None
60 | end
61 |
62 | format_date_in_march(nil) # => "not in march!"
63 | format_date_in_march(Time.parse('2009-01-01 01:02')) # => "not in march!"
64 | format_date_in_march(Time.parse('2011-03-21 12:34')) # => "20110321"
65 | ```
66 |
67 | Note:
68 | * each step of the chained computations above are functionally isolated
69 | * the value can notionally _start_ as nil, or _become_ nil during a computation, without effecting any other chained computations
70 |
71 | ---
72 | ### Either: handle failures (Left) and successes (Right) in a _functional_ fashion:
73 |
74 | ```ruby
75 | def find_person(name)
76 | case name
77 | when /Jack/i, /John/i
78 | Right(name.capitalize)
79 | else
80 | Left("No such person: #{name.capitalize}")
81 | end
82 | end
83 |
84 | # success looks like this:
85 | find_person("Jack")
86 | # => Right("Jack")
87 |
88 | # failure looks like this:
89 | find_person("Jill")
90 | # => Left("No such person: Jill")
91 |
92 | # lift the contained values into Array, in order to combine them:
93 | find_person("Joan").lift_to_a
94 | # => Left(["No such person: Joan"])
95 |
96 | # on the 'happy path', combine and transform successes into a single success result:
97 | (find_person("Jack").lift_to_a +
98 | find_person("John").lift_to_a).right.map { |*names| names.join(" and ") }
99 | # => Right("Jack and John")
100 |
101 | # but if there were errors, we still have a Left with all the errors inside:
102 | (find_person("Jack").lift_to_a +
103 | find_person("John").lift_to_a +
104 | find_person("Jill").lift_to_a +
105 | find_person("Joan").lift_to_a).right.map { |*names| names.join(" and ") }
106 | # => Left(["No such person: Jill", "No such person: Joan"])
107 |
108 | # equivalent to the previous example, but shorter:
109 | %w(Jack John Jill Joan).
110 | map { |nm| find_person(nm).lift_to_a }.inject(:+).
111 | right.map { |*names| names.join(" and ") }
112 | # => Left(["No such person: Jill", "No such person: Joan"])
113 | ```
114 |
115 | Also, see the `Either` class in action in the [Ruby version](https://gist.github.com/2553490)
116 | of [A Tale of Three Nightclubs](http://bugsquash.blogspot.com/2012/03/example-of-applicative-validation-in.html)
117 | validation example in F#, and compare it to the [Scala version using scalaz](https://gist.github.com/970717).
118 |
119 | ---
120 | ### Hash: `flat_map` returns a Hash for each key/value pair; `get` returns an Option
121 |
122 | ```ruby
123 | h = { "Foo" => 1, "Bar" => 2, "Baz" => 3 }
124 |
125 | h = h.flat_map { |k, v| { k => v, k.upcase => v * 10 } }
126 | # => {"Foo"=>1, "FOO"=>10, "Bar"=>2, "BAR"=>20, "Baz"=>3, "BAZ"=>30}
127 |
128 | h = h.select { |k, v| k =~ /^b/i }
129 | # => {"Bar"=>2, "BAR"=>20, "Baz"=>3, "BAZ"=>30}
130 |
131 | h.get("Bar")
132 | # => Some(2)
133 |
134 | h.get("Foo")
135 | # => None
136 | ```
137 |
138 | ## Approach
139 |
140 | There have been [many](http://moonbase.rydia.net/mental/writings/programming/monads-in-ruby/00introduction.html)
141 | [posts](http://pretheory.wordpress.com/2008/02/14/the-maybe-monad-in-ruby/)
142 | [and](http://www.valuedlessons.com/2008/01/monads-in-ruby-with-nice-syntax.html)
143 | [discussions](http://stackoverflow.com/questions/2709361/monad-equivalent-in-ruby)
144 | about monads in Ruby, which have sparked a number of approaches.
145 |
146 | Rumonade wants to be a practical drop-in Monad solution that will fit well into the Ruby world.
147 |
148 | The priorities for Rumonade are:
149 |
150 | 1. Practical usability in day-to-day Ruby
151 | * don't mess up normal idioms of the language (e.g., `Hash#map`)
152 | * don't slow down normal idioms of the language (e.g., `Array#map`)
153 | 2. Rubyish-ness of usage
154 | * Monad is a mix-in, requiring methods `self.unit` and `#bind` be implemented by target class
155 | * Prefer blocks to lambda/Procs where possible, but allow both
156 | 3. Equivalent idioms to Scala where possible
157 |
158 | ## Status
159 |
160 | Option, Either, Array, and Hash are already usable.
161 |
162 | Supported Ruby versions: MRI 2.0.0, 1.9.3, 1.9.2, JRuby in 1.9 mode, and Rubinius in 1.9 mode.
163 |
164 | Please try it out, and let me know what you think! Suggestions are always welcome.
165 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "bundler/gem_tasks"
2 | require "rake/testtask"
3 |
4 | task :default => [:test]
5 |
6 | Rake::TestTask.new(:test) do |test|
7 | test.libs << 'lib' << 'test'
8 | test.pattern = 'test/**/*_test.rb'
9 | end
10 |
--------------------------------------------------------------------------------
/lib/rumonade.rb:
--------------------------------------------------------------------------------
1 | #--
2 | # Copyright (c) 2011 Marc Siegel
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining
5 | # a copy of this software and associated documentation files (the
6 | # "Software"), to deal in the Software without restriction, including
7 | # without limitation the rights to use, copy, modify, merge, publish,
8 | # distribute, sublicense, and/or sell copies of the Software, and to
9 | # permit persons to whom the Software is furnished to do so, subject to
10 | # the following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be
13 | # included in all copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22 | #++
23 | require "rumonade/version"
24 | require "rumonade/errors"
25 | require "rumonade/monad"
26 | require "rumonade/lazy_identity"
27 | require "rumonade/option"
28 | require "rumonade/array"
29 | require "rumonade/hash"
30 | require "rumonade/either"
31 | require "rumonade/error_handling"
32 |
33 | # = Rumonade
34 | #
35 | # Rumonade is a ruby module providing a scala-like system of monads, including:
36 | # * Option
37 | # * Array
38 | #
39 | # See the examples in link:files/README_rdoc.html for more insight.
40 | #
41 | module Rumonade
42 | end
43 |
44 | # bring into global namespace Monad, Option, etc
45 | include Rumonade
46 |
--------------------------------------------------------------------------------
/lib/rumonade/array.rb:
--------------------------------------------------------------------------------
1 | require 'rumonade/monad'
2 |
3 | module Rumonade
4 | # TODO: Document use of Array as a Monad
5 | module ArrayExtensions
6 | module ClassMethods
7 | def unit(value)
8 | [value]
9 | end
10 |
11 | def empty
12 | []
13 | end
14 | end
15 |
16 | module InstanceMethods
17 | # Preserve native +map+ and +flat_map+ methods for performance,
18 | # and +flatten+ to support Rails (see issue #8)
19 | METHODS_TO_REPLACE_WITH_MONAD = []
20 |
21 | def bind(lam = nil, &blk)
22 | inject(self.class.empty) { |arr, elt| arr + (lam || blk).call(elt).to_a }
23 | end
24 | end
25 | end
26 | end
27 |
28 | Array.send(:extend, Rumonade::ArrayExtensions::ClassMethods)
29 | Array.send(:include, Rumonade::ArrayExtensions::InstanceMethods)
30 | Array.send(:include, Rumonade::Monad)
31 |
--------------------------------------------------------------------------------
/lib/rumonade/either.rb:
--------------------------------------------------------------------------------
1 | require 'rumonade/monad'
2 |
3 | module Rumonade
4 | # Represents a value of one of two possible types (a disjoint union).
5 | # The data constructors {Rumonade::Left} and {Rumonade::Right} represent the two possible values.
6 | # The +Either+ type is often used as an alternative to {Rumonade::Option} where {Rumonade::Left} represents
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 | #
12 | # @abstract
13 | class Either
14 | def initialize
15 | raise(TypeError, "class Either is abstract; cannot be instantiated") if self.class == Either
16 | end
17 | private :initialize
18 |
19 | # @return [Boolean] Returns +true+ if this is a {Rumonade::Left}, +false+ otherwise.
20 | def left?
21 | is_a?(Left)
22 | end
23 |
24 | # @return [Boolean] Returns +true+ if this is a {Rumonade::Right}, +false+ otherwise.
25 | def right?
26 | is_a?(Right)
27 | end
28 |
29 | # @return [Boolean] If this is a Left, then return the left value in Right or vice versa.
30 | def swap
31 | if left? then Right(left_value) else Left(right_value) end
32 | end
33 |
34 | # @param [Proc] function_of_left_value the function to apply if this is a Left
35 | # @param [Proc] function_of_right_value the function to apply if this is a Right
36 | # @return Returns the results of applying the function
37 | def fold(function_of_left_value, function_of_right_value)
38 | if left? then function_of_left_value.call(left_value) else function_of_right_value.call(right_value) end
39 | end
40 |
41 | # @return [LeftProjection] Projects this Either as a Left.
42 | def left
43 | LeftProjection.new(self)
44 | end
45 |
46 | # @return [RightProjection] Projects this Either as a Right.
47 | def right
48 | RightProjection.new(self)
49 | end
50 |
51 | # Default concatenation function used by {#+}
52 | DEFAULT_CONCAT = lambda { |a,b| a + b }
53 |
54 | # @param [Either] other the other +Either+ to concatenate
55 | # @param [Hash] opts the options to concatenate with
56 | # @option opts [Proc] :concat_left (DEFAULT_CONCAT) The function to concatenate +Left+ values
57 | # @option opts [Proc] :concat_right (DEFAULT_CONCAT) the function to concatenate +Right+ values
58 | # @yield [right_value] optional block to transform concatenated +Right+ values
59 | # @yieldparam [Object] right_values the concatenated +Right+ values yielded to optional block
60 | # @return [Either] if both are +Right+, returns +Right+ with +right_value+'s concatenated,
61 | # otherwise a +Left+ with +left_value+'s concatenated
62 | def +(other, opts = {})
63 | opts = { :concat_left => DEFAULT_CONCAT, :concat_right => DEFAULT_CONCAT }.merge(opts)
64 | result =
65 | case self
66 | when Left
67 | case other
68 | when Left then Left(opts[:concat_left].call(self.left_value, other.left_value))
69 | when Right then Left(self.left_value)
70 | end
71 | when Right
72 | case other
73 | when Left then Left(other.left_value)
74 | when Right then Right(opts[:concat_right].call(self.right_value, other.right_value))
75 | end
76 | end
77 | if block_given? then result.right.map { |right_values| yield right_values } else result end
78 | end
79 | alias_method :concat, :+
80 |
81 | # @return [Either] returns an +Either+ of the same type, with the +left_value+ or +right_value+
82 | # lifted into an +Array+
83 | def lift_to_a
84 | lift(Array)
85 | end
86 |
87 | # @param [#unit] monad_class the {Monad} to lift the +Left+ or +Right+ value into
88 | # @return [Either] returns an +Either+of the same type, with the +left_value+ or +right_value+
89 | # lifted into +monad_class+
90 | def lift(monad_class)
91 | fold(lambda {|l| Left(monad_class.unit(l)) }, lambda {|r| Right(monad_class.unit(r))})
92 | end
93 | end
94 |
95 | # The left side of the disjoint union, as opposed to the Right side.
96 | class Left < Either
97 | # @param left_value the value to store in a +Left+, usually representing a failure result
98 | def initialize(left_value)
99 | @left_value = left_value
100 | end
101 |
102 | # @return Returns the left value
103 | attr_reader :left_value
104 |
105 | # @return [Boolean] Returns +true+ if other is a +Left+ with an equal left value
106 | def ==(other)
107 | other.is_a?(Left) && other.left_value == self.left_value
108 | end
109 |
110 | # @return [String] Returns a +String+ representation of this object.
111 | def to_s
112 | "Left(#{left_value})"
113 | end
114 |
115 | # @return [String] Returns a +String+ containing a human-readable representation of this object.
116 | def inspect
117 | "Left(#{left_value.inspect})"
118 | end
119 | end
120 |
121 | # The right side of the disjoint union, as opposed to the Left side.
122 | class Right < Either
123 | # @param right_value the value to store in a +Right+, usually representing a success result
124 | def initialize(right_value)
125 | @right_value = right_value
126 | end
127 |
128 | # @return Returns the right value
129 | attr_reader :right_value
130 |
131 | # @return [Boolean] Returns +true+ if other is a +Right+ with an equal right value
132 | def ==(other)
133 | other.is_a?(Right) && other.right_value == self.right_value
134 | end
135 |
136 | # @return [String] Returns a +String+ representation of this object.
137 | def to_s
138 | "Right(#{right_value})"
139 | end
140 |
141 | # @return [String] Returns a +String+ containing a human-readable representation of this object.
142 | def inspect
143 | "Right(#{right_value.inspect})"
144 | end
145 | end
146 |
147 | # @param (see Left#initialize)
148 | # @return [Left]
149 | def Left(left_value)
150 | Left.new(left_value)
151 | end
152 |
153 | # @param (see Right#initialize)
154 | # @return [Right]
155 | def Right(right_value)
156 | Right.new(right_value)
157 | end
158 |
159 | class Either
160 | # Projects an Either into a Left.
161 | class LeftProjection
162 | class << self
163 | # @return [LeftProjection] Returns a +LeftProjection+ of the +Left+ of the given value
164 | def unit(value)
165 | self.new(Left(value))
166 | end
167 |
168 | # @return [LeftProjection] Returns the empty +LeftProjection+
169 | def empty
170 | self.new(Right(nil))
171 | end
172 | end
173 |
174 | # @param either_value [Object] the Either value to project
175 | def initialize(either_value)
176 | @either_value = either_value
177 | end
178 |
179 | # @return Returns the Either value
180 | attr_reader :either_value
181 |
182 | # @return [Boolean] Returns +true+ if other is a +LeftProjection+ with an equal +Either+ value
183 | def ==(other)
184 | other.is_a?(LeftProjection) && other.either_value == self.either_value
185 | end
186 |
187 | # Binds the given function across +Left+.
188 | def bind(lam = nil, &blk)
189 | if !either_value.left? then either_value else (lam || blk).call(either_value.left_value) end
190 | end
191 |
192 | include Monad
193 |
194 | # @return [Boolean] Returns +false+ if +Right+ or returns the result of the application of the given function to the +Left+ value.
195 | def any?(lam = nil, &blk)
196 | either_value.left? && bind(lam || blk)
197 | end
198 |
199 | # @return [Option] Returns +None+ if this is a +Right+ or if the given predicate does not hold for the +left+ value, otherwise, returns a +Some+ of +Left+.
200 | def select(lam = nil, &blk)
201 | Some(self).select { |lp| lp.any?(lam || blk) }.map { |lp| lp.either_value }
202 | end
203 |
204 | # @return [Boolean] Returns +true+ if +Right+ or returns the result of the application of the given function to the +Left+ value.
205 | def all?(lam = nil, &blk)
206 | !either_value.left? || bind(lam || blk)
207 | end
208 |
209 | # Returns the value from this +Left+ or raises +NoSuchElementException+ if this is a +Right+.
210 | def get
211 | if either_value.left? then either_value.left_value else raise NoSuchElementError end
212 | end
213 |
214 | # Returns the value from this +Left+ or the given argument if this is a +Right+.
215 | def get_or_else(val_or_lam = nil, &blk)
216 | v_or_f = val_or_lam || blk
217 | if either_value.left? then either_value.left_value else (v_or_f.respond_to?(:call) ? v_or_f.call : v_or_f) end
218 | end
219 |
220 | # @return [Option] Returns a +Some+ containing the +Left+ value if it exists or a +None+ if this is a +Right+.
221 | def to_opt
222 | Option(get_or_else(nil))
223 | end
224 |
225 | # @return [Either] Maps the function argument through +Left+.
226 | def map(lam = nil, &blk)
227 | bind { |v| Left((lam || blk).call(v)) }
228 | end
229 |
230 | # @return [String] Returns a +String+ representation of this object.
231 | def to_s
232 | "LeftProjection(#{either_value})"
233 | end
234 |
235 | # @return [String] Returns a +String+ containing a human-readable representation of this object.
236 | def inspect
237 | "LeftProjection(#{either_value.inspect})"
238 | end
239 | end
240 |
241 | # Projects an Either into a Right.
242 | class RightProjection
243 | class << self
244 | # @return [RightProjection] Returns a +RightProjection+ of the +Right+ of the given value
245 | def unit(value)
246 | self.new(Right(value))
247 | end
248 |
249 | # @return [RightProjection] Returns the empty +RightProjection+
250 | def empty
251 | self.new(Left(nil))
252 | end
253 | end
254 |
255 | # @param either_value [Object] the Either value to project
256 | def initialize(either_value)
257 | @either_value = either_value
258 | end
259 |
260 | # @return Returns the Either value
261 | attr_reader :either_value
262 |
263 | # @return [Boolean] Returns +true+ if other is a +RightProjection+ with an equal +Either+ value
264 | def ==(other)
265 | other.is_a?(RightProjection) && other.either_value == self.either_value
266 | end
267 |
268 | # Binds the given function across +Right+.
269 | def bind(lam = nil, &blk)
270 | if !either_value.right? then either_value else (lam || blk).call(either_value.right_value) end
271 | end
272 |
273 | include Monad
274 |
275 | # @return [Boolean] Returns +false+ if +Left+ or returns the result of the application of the given function to the +Right+ value.
276 | def any?(lam = nil, &blk)
277 | either_value.right? && bind(lam || blk)
278 | end
279 |
280 | # @return [Option] Returns +None+ if this is a +Left+ or if the given predicate does not hold for the +Right+ value, otherwise, returns a +Some+ of +Right+.
281 | def select(lam = nil, &blk)
282 | Some(self).select { |lp| lp.any?(lam || blk) }.map { |lp| lp.either_value }
283 | end
284 |
285 | # @return [Boolean] Returns +true+ if +Left+ or returns the result of the application of the given function to the +Right+ value.
286 | def all?(lam = nil, &blk)
287 | !either_value.right? || bind(lam || blk)
288 | end
289 |
290 | # Returns the value from this +Right+ or raises +NoSuchElementException+ if this is a +Left+.
291 | def get
292 | if either_value.right? then either_value.right_value else raise NoSuchElementError end
293 | end
294 |
295 | # Returns the value from this +Right+ or the given argument if this is a +Left+.
296 | def get_or_else(val_or_lam = nil, &blk)
297 | v_or_f = val_or_lam || blk
298 | if either_value.right? then either_value.right_value else (v_or_f.respond_to?(:call) ? v_or_f.call : v_or_f) end
299 | end
300 |
301 | # @return [Option] Returns a +Some+ containing the +Right+ value if it exists or a +None+ if this is a +Left+.
302 | def to_opt
303 | Option(get_or_else(nil))
304 | end
305 |
306 | # @return [Either] Maps the function argument through +Right+.
307 | def map(lam = nil, &blk)
308 | bind { |v| Right((lam || blk).call(v)) }
309 | end
310 |
311 | # @return [String] Returns a +String+ representation of this object.
312 | def to_s
313 | "RightProjection(#{either_value})"
314 | end
315 |
316 | # @return [String] Returns a +String+ containing a human-readable representation of this object.
317 | def inspect
318 | "RightProjection(#{either_value.inspect})"
319 | end
320 | end
321 | end
322 |
323 | module_function :Left, :Right
324 | public :Left, :Right
325 | end
--------------------------------------------------------------------------------
/lib/rumonade/error_handling.rb:
--------------------------------------------------------------------------------
1 | require "delegate"
2 |
3 | module Rumonade
4 |
5 | # A partial function is a unary function where the domain does not necessarily include all values.
6 | # The function {#defined_at?} allows to test dynamically if a value is in the domain of the function.
7 | #
8 | # NOTE: This is only here to mimic the Scala library just enough to allow a close translation of the exception
9 | # handling functionality. It's not because I'm the sort that just loves pure functional idioms so damn much for
10 | # their own sake. Just FYI.
11 | class PartialFunction < DelegateClass(Proc)
12 | def initialize(defined_at_proc, call_proc)
13 | super(call_proc)
14 | @defined_at_proc = defined_at_proc
15 | end
16 |
17 | # Checks if a value is contained in the function's domain.
18 | # @param x the value to test
19 | # @return [Boolean] Returns +true+, iff +x+ is in the domain of this function, +false+ otherwise.
20 | def defined_at?(x)
21 | @defined_at_proc.call(x)
22 | end
23 |
24 | # Composes this partial function with a fallback partial function which
25 | # gets applied where this partial function is not defined.
26 | # @param [PartialFunction] other the fallback function
27 | # @return [PartialFunction] a partial function which has as domain the union of the domains
28 | # of this partial function and +other+. The resulting partial function takes +x+ to +self.call(x)+
29 | # where +self+ is defined, and to +other.call(x)+ where it is not.
30 | def or_else(other)
31 | PartialFunction.new(lambda { |x| self.defined_at?(x) || other.defined_at?(x) },
32 | lambda { |x| if self.defined_at?(x) then self.call(x) else other.call(x) end })
33 | end
34 |
35 | # Composes this partial function with a transformation function that
36 | # gets applied to results of this partial function.
37 | # @param [Proc] func the transformation function
38 | # @return [PartialFunction] a partial function with the same domain as this partial function, which maps
39 | # arguments +x+ to +func.call(self.call(x))+.
40 | def and_then(func)
41 | PartialFunction.new(@defined_at_proc, lambda { |x| func.call(self.call(x)) })
42 | end
43 | end
44 |
45 | # Classes representing the components of exception handling.
46 | # Each class is independently composable. Some example usages:
47 | #
48 | # require "rumonade"
49 | # require "uri"
50 | #
51 | # s = "http://"
52 | # x1 = catching(URI::InvalidURIError).opt { URI.parse(s) }
53 | # x2 = catching(URI::InvalidURIError, NoMethodError).either { URI.parse(s) }
54 | #
55 | module ErrorHandling
56 |
57 | # Should re-raise exceptions like +Interrupt+ and +NoMemoryError+ by default.
58 | # @param [Exception] ex the exception to consider re-raising
59 | # @return [Boolean] Returns +true+ if a subclass of +StandardError+, otherwise +false+.
60 | def should_reraise?(ex)
61 | case ex
62 | when StandardError; false
63 | else true
64 | end
65 | end
66 | end
67 | end
--------------------------------------------------------------------------------
/lib/rumonade/errors.rb:
--------------------------------------------------------------------------------
1 | module Rumonade
2 |
3 | # Exception raised on attempts to access non-existent wrapped values
4 | class NoSuchElementError < RuntimeError; end
5 |
6 | end
--------------------------------------------------------------------------------
/lib/rumonade/hash.rb:
--------------------------------------------------------------------------------
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 |
31 | module InstanceOverrides
32 | def can_flatten_in_monad?
33 | false
34 | end
35 | end
36 | end
37 | end
38 |
39 | Hash.send(:extend, Rumonade::HashExtensions::ClassMethods)
40 | Hash.send(:include, Rumonade::HashExtensions::InstanceMethods)
41 | Hash.send(:include, Rumonade::Monad)
42 | Hash.send(:include, Rumonade::HashExtensions::InstanceOverrides)
43 |
--------------------------------------------------------------------------------
/lib/rumonade/lazy_identity.rb:
--------------------------------------------------------------------------------
1 | # Adapted from http://stackoverflow.com/questions/2709361/monad-equivalent-in-ruby
2 | class LazyIdentity # :nodoc:
3 | def initialize(lam = nil, &blk)
4 | @lazy = lam || blk
5 | @lazy.is_a?(Proc) || raise(ArgumentError, "not a Proc")
6 | @lazy.arity.zero? || raise(ArgumentError, "arity must be 0, was #{@lazy.arity}")
7 | end
8 |
9 | attr_reader :lazy
10 |
11 | def force
12 | @lazy[]
13 | end
14 |
15 | def self.unit(lam = nil, &blk)
16 | LazyIdentity.new(lam || blk)
17 | end
18 |
19 | def bind(lam = nil, &blk)
20 | f = lam || blk
21 | f[@lazy]
22 | end
23 |
24 | def ==(other)
25 | other.is_a?(LazyIdentity) && other.force == force
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/lib/rumonade/monad.rb:
--------------------------------------------------------------------------------
1 | module Rumonade
2 | # Mix-in for common monad functionality dependent on implementation of monadic methods +unit+ and +bind+
3 | #
4 | # Notes:
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+
9 | #
10 | module Monad
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
17 |
18 | base.class_eval do
19 | # optimization: replace flat_map with an alias for bind, as they are identical
20 | alias_method :flat_map_with_monad, :bind
21 |
22 | methods_to_replace.each do |method_name|
23 | alias_method "#{method_name}_without_monad".to_sym, method_name if public_instance_methods.include? method_name
24 | alias_method method_name, "#{method_name}_with_monad".to_sym
25 | end
26 | end
27 | end
28 |
29 | include Enumerable
30 |
31 | # Applies the given procedure to each element in this monad
32 | def each(lam = nil, &blk)
33 | bind { |v| (lam || blk).call(v) }; nil
34 | end
35 |
36 | # Returns a monad whose elements are the results of applying the given function to each element in this monad
37 | #
38 | # NOTE: normally aliased as +map+ when +Monad+ is mixed into a class
39 | def map_with_monad(lam = nil, &blk)
40 | bind { |v| self.class.unit((lam || blk).call(v)) }
41 | end
42 |
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
46 | def flat_map_with_monad(lam = nil, &blk)
47 | bind(lam || blk)
48 | end
49 |
50 | # Returns a monad whose elements are the ultimate (non-monadic) values contained in all nested monads
51 | #
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_with_monad
56 | # #=> [1]
57 | #
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 do |x|
63 | if x.is_a?(Monad) && x.can_flatten_in_monad?
64 | x.flatten_with_monad
65 | else
66 | self.class.unit(x)
67 | end
68 | end
69 | end
70 | end
71 |
72 | # Returns a monad whose elements are all those elements of this monad for which the given predicate returned true
73 | def select(lam = nil, &blk)
74 | bind { |x| (lam || blk).call(x) ? self.class.unit(x) : self.class.empty }
75 | end
76 | alias_method :find_all, :select
77 |
78 | # Returns a monad whose elements are the values contained in the first level of nested monads
79 | #
80 | # This method is equivalent to the Scala flatten call (single-level flattening), whereas #flatten is in keeping
81 | # with the native Ruby flatten calls (multiple-level flattening).
82 | #
83 | # @example
84 | # [Some(Some(1)), Some(Some(None)), [None]].shallow_flatten
85 | # #=> [Some(1), Some(None), None]
86 | # [Some(1), Some(None), None].shallow_flatten
87 | # #=> [1, None]
88 | # [1, None].shallow_flatten
89 | # #=> [1]
90 | #
91 | def shallow_flatten
92 | bind { |x| x.is_a?(Monad) ? x : self.class.unit(x) }
93 | end
94 |
95 | # Returns +true+ if #flatten_with_monad can call recursively on contained
96 | # values members (eg. elements of an array).
97 | #
98 | # NOTE: Is overridden in Hash.
99 | def can_flatten_in_monad?
100 | true
101 | end
102 | end
103 | end
104 |
--------------------------------------------------------------------------------
/lib/rumonade/option.rb:
--------------------------------------------------------------------------------
1 | require 'singleton'
2 | require 'rumonade/monad'
3 |
4 | module Rumonade # :nodoc:
5 | # Represents optional values. Instances of Option are either an instance of Some or the object None.
6 | #
7 | # The most idiomatic way to use an Option instance is to treat it as a collection or monad
8 | # and use map, flat_map, select, or each:
9 | #
10 | # name = Option(params[:name])
11 | # upper = name.map(&:strip).select { |s| s.length != 0 }.map(&:upcase)
12 | # puts upper.get_or_else("")
13 | #
14 | # Note that this is equivalent to
15 | #
16 | # # TODO: IMPLEMENT FOR COMPREHENSIONS
17 | # # see http://stackoverflow.com/questions/1052476/can-someone-explain-scalas-yield
18 | # val upper = for {
19 | # name <- Option(params[:name])
20 | # trimmed <- Some(name.strip)
21 | # upper <- Some(trimmed.upcase) if trimmed.length != 0
22 | # } yield upper
23 | # puts upper.get_or_else("")
24 | #
25 | # Because of how for comprehension works, if None is returned from params#[], the entire expression results in None
26 | # This allows for sophisticated chaining of Option values without having to check for the existence of a value.
27 | #
28 | # A less-idiomatic way to use Option values is via direct comparison:
29 | #
30 | # name_opt = params[:name]
31 | # case name_opt
32 | # when Some
33 | # puts name_opt.get.strip.upcase
34 | # when None
35 | # puts "No name value"
36 | # end
37 | #
38 | # @abstract
39 | class Option
40 | class << self
41 | # @return [Option] Returns an +Option+ containing the given value
42 | def unit(value)
43 | Rumonade.Some(value)
44 | end
45 |
46 | # @return [Option] Returns the empty +Option+
47 | def empty
48 | None
49 | end
50 | end
51 |
52 | def initialize
53 | raise(TypeError, "class Option is abstract; cannot be instantiated") if self.class == Option
54 | end
55 | private :initialize
56 |
57 | # Returns None if None, or the result of executing the given block or lambda on the contents if Some
58 | def bind(lam = nil, &blk)
59 | empty? ? self : (lam || blk).call(value)
60 | end
61 |
62 | include Monad
63 |
64 | # @return [Boolean] Returns +true+ if +None+, +false+ if +Some+
65 | def empty?
66 | raise(NotImplementedError)
67 | end
68 |
69 | # Returns contents if Some, or raises NoSuchElementError if None
70 | def get
71 | if !empty? then value else raise NoSuchElementError end
72 | end
73 |
74 | # Returns contents if Some, or given value or result of given block or lambda if None
75 | def get_or_else(val_or_lam = nil, &blk)
76 | v_or_f = val_or_lam || blk
77 | if !empty? then value else (v_or_f.respond_to?(:call) ? v_or_f.call : v_or_f) end
78 | end
79 |
80 | # Returns contents if Some, or +nil+ if None
81 | def or_nil
82 | get_or_else(nil)
83 | end
84 | end
85 |
86 | # Represents an Option containing a value
87 | class Some < Option
88 | def initialize(value)
89 | @value = value
90 | end
91 |
92 | attr_reader :value # :nodoc:
93 |
94 | # @return (see Option#empty?)
95 | def empty?
96 | false
97 | end
98 |
99 | def ==(other)
100 | other.is_a?(Some) && other.value == value
101 | end
102 |
103 | def to_s
104 | "Some(#{value.nil? ? 'nil' : value.to_s})"
105 | end
106 | end
107 |
108 | # Represents an Option which is empty, accessed via the constant None
109 | class NoneClass < Option
110 | include Singleton
111 |
112 | # @return (see Option#empty?)
113 | def empty?
114 | true
115 | end
116 |
117 | def ==(other)
118 | other.equal?(self.class.instance)
119 | end
120 |
121 | def to_s
122 | "None"
123 | end
124 | end
125 |
126 | # Returns an Option wrapping the given value: Some if non-nil, None if nil
127 | def Option(value)
128 | value.nil? ? None : Some(value)
129 | end
130 |
131 | # @return [Some] Returns a +Some+ wrapping the given value, for convenience
132 | def Some(value)
133 | Some.new(value)
134 | end
135 |
136 | # The single global instance of NoneClass, representing the empty Option
137 | None = NoneClass.instance # :doc:
138 |
139 | module_function :Option, :Some
140 | public :Option, :Some
141 | end
142 |
--------------------------------------------------------------------------------
/lib/rumonade/version.rb:
--------------------------------------------------------------------------------
1 | module Rumonade
2 | VERSION = "0.4.4"
3 | end
4 |
--------------------------------------------------------------------------------
/rumonade.gemspec:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | $:.push File.expand_path('../lib', __FILE__)
3 | require 'rumonade/version'
4 |
5 | Gem::Specification.new do |s|
6 | s.name = 'rumonade'
7 | s.version = Rumonade::VERSION
8 | s.authors = ['Marc Siegel']
9 | s.email = ['marc@usainnov.com']
10 | s.homepage = 'http://github.com/ms-ati/rumonade'
11 | s.summary = 'A Scala-inspired Monad library for Ruby'
12 | s.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.'
13 | s.license = 'MIT'
14 |
15 | s.files = `git ls-files`.split("\n")
16 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18 | s.require_paths = ['lib']
19 |
20 | # specify any dependencies here; for example:
21 | s.add_development_dependency 'test-unit'
22 | s.add_development_dependency 'rake'
23 | s.add_development_dependency 'coveralls'
24 | end
25 |
--------------------------------------------------------------------------------
/test/array_test.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.dirname(__FILE__) + '/test_helper')
2 |
3 | class ArrayTest < Test::Unit::TestCase
4 | include Rumonade
5 | include MonadAxiomTestHelpers
6 |
7 | def test_when_unit_returns_1_elt_array
8 | assert_equal [1], Array.unit(1)
9 | end
10 |
11 | def test_when_empty_returns_empty_array
12 | assert_equal [], Array.empty
13 | end
14 |
15 | def test_monad_axioms
16 | f = lambda { |x| Array.unit(x && x * 2) }
17 | g = lambda { |x| Array.unit(x && x * 5) }
18 | [1, 42].each do |value|
19 | assert_monad_axiom_1(Array, value, f)
20 | assert_monad_axiom_2(Array.unit(value))
21 | assert_monad_axiom_3(Array.unit(value), f, g)
22 | end
23 | end
24 |
25 | def test_flat_map_behaves_correctly
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] }
28 | end
29 |
30 | def test_map_behaves_correctly
31 | assert_equal ["FOO", "BAR"], ["foo", "bar"].map { |s| s.upcase }
32 | assert_equal [2, 4, 6], [1, 2, 3].map { |i| i * 2 }
33 | end
34 |
35 | def test_shallow_flatten_behaves_correctly
36 | assert_equal [0, 1, [2], [[3]], [[[4]]]], [0, [1], [[2]], [[[3]]], [[[[4]]]]].shallow_flatten
37 | assert_equal [1], [None, Some(1)].shallow_flatten
38 | assert_equal [1, Some(2)], [None, Some(1), Some(Some(2))].shallow_flatten
39 | assert_equal [Some(Some(None))], [Some(Some(Some(None)))].shallow_flatten
40 | assert_equal [Some(1), Some(None), None], [Some(Some(1)), Some(Some(None)), [None]].shallow_flatten
41 | end
42 |
43 | def test_flatten_behaves_correctly
44 | assert_equal [0, 1, 2, 3, 4], [0, [1], [[2]], [[[3]]], [[[[4]]]]].flatten_with_monad
45 | assert_equal [1, 2], [None, Some(1), Some(Some(2))].flatten_with_monad
46 | assert_equal [], [Some(Some(Some(None)))].flatten_with_monad
47 | end
48 |
49 | def test_flatten_with_argument_behaves_correctly
50 | assert_equal [0, 1, [2], [[3]], [[[4]]]], [0, [1], [[2]], [[[3]]], [[[[4]]]]].flatten_with_monad(1)
51 | assert_equal [0, 1, 2, [3], [[4]]], [0, [1], [[2]], [[[3]]], [[[[4]]]]].flatten_with_monad(2)
52 | assert_equal [0, 1, 2, 3, [4]], [0, [1], [[2]], [[[3]]], [[[[4]]]]].flatten_with_monad(3)
53 | assert_equal [0, 1, 2, 3, 4], [0, [1], [[2]], [[[3]]], [[[[4]]]]].flatten_with_monad(4)
54 | assert_equal [Some(Some(1)), Some(Some(None)), [None]], [Some(Some(1)), Some(Some(None)), [None]].flatten_with_monad(0)
55 | assert_equal [Some(1), Some(None), None], [Some(Some(1)), Some(Some(None)), [None]].flatten_with_monad(1)
56 | assert_equal [1, None], [Some(Some(1)), Some(Some(None)), [None]].flatten_with_monad(2)
57 | assert_equal [1], [Some(Some(1)), Some(Some(None)), [None]].flatten_with_monad(3)
58 | end
59 |
60 | def test_flatten_does_not_break_default_ruby_behaviour_with_nested_hash
61 | arr = [ { 'thou' => 'shalt', 'not touch' => 'hashes' }, ', seriously!' ]
62 | assert_equal arr, arr.flatten_with_monad
63 | end
64 | end
65 |
--------------------------------------------------------------------------------
/test/either_test.rb:
--------------------------------------------------------------------------------
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 |
54 | def test_any_predicate_for_left_and_right_projections_returns_true_if_correct_type_and_block_returns_true
55 | assert Left("error").left.any? { |s| s == "error" }
56 | assert !Left("error").left.any? { |s| s != "error" }
57 | assert !Left("error").right.any? { |s| s == "error" }
58 |
59 | assert Right(42).right.any? { |n| n == 42 }
60 | assert !Right(42).right.any? { |n| n != 42 }
61 | assert !Right(42).left.any? { |n| n == 42 }
62 | end
63 |
64 | def test_select_for_left_and_right_projects_returns_option_of_either_if_correct_type_and_block_returns_true
65 | assert_equal Some(Left("error")), Left("error").left.select { |s| s == "error" }
66 | assert_equal None, Left("error").left.select { |s| s != "error" }
67 | assert_equal None, Left("error").right.select { |s| s == "error" }
68 |
69 | assert_equal Some(Right(42)), Right(42).right.select { |n| n == 42 }
70 | assert_equal None, Right(42).right.select { |n| n != 42 }
71 | assert_equal None, Right(42).left.select { |n| n == 42 }
72 | end
73 |
74 | def test_all_predicate_for_left_and_right_projections_returns_true_if_correct_type_and_block_returns_true
75 | assert Left("error").left.all? { |s| s == "error" }
76 | assert !Left("error").left.all? { |s| s != "error" }
77 | assert Left("error").right.all? { |s| s == "error" }
78 |
79 | assert Right(42).right.all? { |n| n == 42 }
80 | assert !Right(42).right.all? { |n| n != 42 }
81 | assert Right(42).left.all? { |n| n == 42 }
82 | end
83 |
84 | def test_each_for_left_and_right_projections_executes_block_if_correct_type
85 | def side_effect_occurred_on_each(projection)
86 | side_effect_occurred = false
87 | projection.each { |s| side_effect_occurred = true }
88 | side_effect_occurred
89 | end
90 |
91 | assert side_effect_occurred_on_each(Left("error").left)
92 | assert !side_effect_occurred_on_each(Left("error").right)
93 |
94 | assert side_effect_occurred_on_each(Right(42).right)
95 | assert !side_effect_occurred_on_each(Right(42).left)
96 | end
97 |
98 | def test_unit_for_left_and_right_projections
99 | assert_equal Left("error").left, Either::LeftProjection.unit("error")
100 | assert_equal Right(42).right, Either::RightProjection.unit(42)
101 | end
102 |
103 | def test_empty_for_left_and_right_projections
104 | assert_equal Right(nil).left, Either::LeftProjection.empty
105 | assert_equal Left(nil).right, Either::RightProjection.empty
106 | end
107 |
108 | def test_monad_axioms_for_left_and_right_projections
109 | assert_monad_axiom_1(Either::LeftProjection, "error", lambda { |x| Left(x * 2).left })
110 | assert_monad_axiom_2(Left("error").left)
111 | assert_monad_axiom_3(Left("error").left, lambda { |x| Left(x * 2).left }, lambda { |x| Left(x * 5).left })
112 |
113 | assert_monad_axiom_1(Either::RightProjection, 42, lambda { |x| Right(x * 2).right })
114 | assert_monad_axiom_2(Right(42).right)
115 | assert_monad_axiom_3(Right(42).right, lambda { |x| Right(x * 2).right }, lambda { |x| Right(x * 5).right })
116 | end
117 |
118 | def test_get_for_left_and_right_projections_returns_value_if_correct_type_or_raises
119 | assert_equal "error", Left("error").left.get
120 | assert_raises(NoSuchElementError) { Left("error").right.get }
121 | assert_equal 42, Right(42).right.get
122 | assert_raises(NoSuchElementError) { Right(42).left.get }
123 | end
124 |
125 | def test_get_or_else_for_left_and_right_projections_returns_value_if_correct_type_or_returns_value_or_executes_block
126 | assert_equal "error", Left("error").left.get_or_else(:other_value)
127 | assert_equal :other_value, Left("error").right.get_or_else(:other_value)
128 | assert_equal :value_of_block, Left("error").right.get_or_else(lambda { :value_of_block })
129 |
130 | assert_equal 42, Right(42).right.get_or_else(:other_value)
131 | assert_equal :other_value, Right(42).left.get_or_else(:other_value)
132 | assert_equal :value_of_block, Right(42).left.get_or_else(lambda { :value_of_block })
133 | end
134 |
135 | def test_to_opt_for_left_and_right_projections_returns_Some_if_correct_type_or_None
136 | assert_equal Some("error"), Left("error").left.to_opt
137 | assert_equal None, Left("error").right.to_opt
138 | assert_equal Some(42), Right(42).right.to_opt
139 | assert_equal None, Right(42).left.to_opt
140 | end
141 |
142 | def test_to_a_for_left_and_right_projections_returns_single_element_Array_if_correct_type_or_zero_element_Array
143 | assert_equal ["error"], Left("error").left.to_a
144 | assert_equal [], Left("error").right.to_a
145 | assert_equal [42], Right(42).right.to_a
146 | assert_equal [], Right(42).left.to_a
147 | end
148 |
149 | def test_map_for_left_and_right_projections_returns_same_projection_type_of_new_value_if_correct_type
150 | assert_equal Left(:ERROR), Left("error").left.map { |s| s.upcase.to_sym }
151 | assert_equal Left("error"), Left("error").right.map { |s| s.upcase.to_sym }
152 | assert_equal Right(420), Right(42).right.map { |s| s * 10 }
153 | assert_equal Right(42), Right(42).left.map { |s| s * 10 }
154 | end
155 |
156 | def test_to_s_for_left_and_right_and_their_projections
157 | assert_equal "Left(error)", Left("error").to_s
158 | assert_equal "Right(42)", Right(42).to_s
159 | assert_equal "RightProjection(Left(error))", Left("error").right.to_s
160 | assert_equal "LeftProjection(Right(42))", Right(42).left.to_s
161 | end
162 |
163 | def test_inspect_for_left_and_right_and_their_projections
164 | assert_equal "Left(\"error\")", Left("error").inspect
165 | assert_equal "Right(\"success\")", Right("success").inspect
166 | assert_equal "RightProjection(Left(\"error\"))", Left("error").right.inspect
167 | assert_equal "LeftProjection(Right(\"success\"))", Right("success").left.inspect
168 | end
169 |
170 | def test_plus_concatenates_left_and_right_using_plus_operator
171 | assert_equal Left("badworse"), Left("bad") + Right(1) + Left("worse") + Right(2)
172 | assert_equal Left(["bad", "worse"]), Left(["bad"]) + Right(1) + Left(["worse"]) + Right(2)
173 | assert_equal Right(3), Right(1) + Right(2)
174 | end
175 |
176 | def test_concat_concatenates_left_and_right_with_custom_concatenation_function
177 | multiply = lambda { |a, b| a * b }
178 | assert_equal Left(33), Left(3).concat(Left(11), :concat_left => multiply)
179 | assert_equal Left(14), Left(3).concat(Left(11), :concat_right => multiply)
180 | assert_equal Right(44), Right(4).concat(Right(11), :concat_right => multiply)
181 | assert_equal Right(15), Right(4).concat(Right(11), :concat_left => multiply)
182 | end
183 |
184 | def test_lift_to_a_wraps_left_and_right_values_in_array
185 | assert_equal Left(["error"]), Left("error").lift_to_a
186 | assert_equal Right([42]), Right(42).lift_to_a
187 | end
188 |
189 | def test_plus_and_lift_to_a_work_together_to_concatenate_errors
190 | assert_equal Left([1, 2]), Left(1).lift_to_a + Right(:a).lift_to_a + Left(2).lift_to_a + Right(:b).lift_to_a
191 | assert_equal Right([:a, :b]), Right(:a).lift_to_a + Right(:b).lift_to_a
192 | end
193 |
194 | Person = Struct.new(:name, :age, :address)
195 |
196 | def test_concat_maps_concatenated_right_values_through_a_block
197 | assert_equal Right(Person.new("Joe", 23, ["123 Some St", "Boston"])),
198 | Right(["Joe"]).concat(Right([23])).concat(Right([["123 Some St", "Boston"]])) { |n, a, addr| Person.new(n, a, addr) }
199 | # this usage is equivalent, but since ruby can't pass a block to a binary operator, must use .right.map on result:
200 | assert_equal Right(Person.new("Joe", 23, ["123 Some St", "Boston"])),
201 | (Right(["Joe"]) + Right([23]) + Right([["123 Some St", "Boston"]])).right.map { |n, a, addr| Person.new(n, a, addr) }
202 | end
203 | end
204 |
--------------------------------------------------------------------------------
/test/error_handling_test.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.dirname(__FILE__) + '/test_helper')
2 |
3 | class ErrorHandlingTest < Test::Unit::TestCase
4 | include Rumonade
5 | include Rumonade::ErrorHandling
6 |
7 | def test_partial_function_constructor_delegates_call_and_defined_at_to_given_procs
8 | pf = PartialFunction.new(lambda { |x| x < 1000 }, lambda { |x| x * 10 })
9 | assert pf.defined_at?(999)
10 | assert !pf.defined_at?(1000)
11 | assert_equal 420, pf.call(42)
12 | end
13 |
14 | def test_partial_function_or_else_returns_a_partial_function_with_union_of_defined_at_predicates
15 | pf = PartialFunction.new(lambda { |x| x < 1000 }, lambda { |x| x * 10 })
16 | .or_else(PartialFunction.new(lambda { |x| x > 5000 }, lambda { |x| x / 5 }))
17 | assert pf.defined_at?(999)
18 | assert !pf.defined_at?(1000)
19 | assert !pf.defined_at?(5000)
20 | assert pf.defined_at?(5001)
21 | end
22 |
23 | def test_partial_function_or_else_returns_a_partial_function_with_fallback_when_outside_defined_at
24 | pf = PartialFunction.new(lambda { |x| x < 1000 }, lambda { |x| x * 10 })
25 | .or_else(PartialFunction.new(lambda { |x| x > 5000 }, lambda { |x| x / 5 }))
26 | assert_equal 9990, pf.call(999)
27 | assert_equal 1001, pf.call(5005)
28 | end
29 |
30 | def test_partial_function_and_then_returns_a_partial_function_with_func_called_on_result_of_pf_call
31 | pf = PartialFunction.new(lambda { |x| x < 1000 }, lambda { |x| x * 10 })
32 | .and_then(lambda { |x| x / 5 })
33 | assert_equal 1800, pf.call(900)
34 | end
35 |
36 | def test_should_reraise_returns_true_if_not_subclass_of_standard_error
37 | assert should_reraise?(NoMemoryError.new)
38 | assert !should_reraise?(ZeroDivisionError.new)
39 | end
40 | end
--------------------------------------------------------------------------------
/test/hash_test.rb:
--------------------------------------------------------------------------------
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
--------------------------------------------------------------------------------
/test/lazy_identity_test.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper'))
2 |
3 | class LazyIdentityTest < Test::Unit::TestCase
4 | include Rumonade
5 | include MonadAxiomTestHelpers
6 |
7 | def test_monad_axioms
8 | f = lambda { |lazy| v = lazy.call * 2; LazyIdentity.new { v } }
9 | g = lambda { |lazy| v = lazy.call + 5; LazyIdentity.new { v } }
10 | lazy_value = lambda { 42 } # returns 42 when called
11 | assert_monad_axiom_1(LazyIdentity, lazy_value, f)
12 | assert_monad_axiom_2(LazyIdentity.new(lazy_value))
13 | assert_monad_axiom_3(LazyIdentity.new(lazy_value), f, g)
14 | end
15 |
16 | end
--------------------------------------------------------------------------------
/test/option_test.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path(File.dirname(__FILE__) + '/test_helper')
2 |
3 | class OptionTest < Test::Unit::TestCase
4 | include Rumonade
5 | include MonadAxiomTestHelpers
6 |
7 | def test_when_option_with_nil_returns_none_singleton_except_unit
8 | assert_same None, Option(nil)
9 | assert_same NoneClass.instance, None
10 | assert_not_equal None, Option.unit(nil)
11 | end
12 |
13 | def test_when_option_with_value_returns_some
14 | assert_equal Some(42), Option.unit(42)
15 | assert_equal Some(42), Option(42)
16 | assert_equal Some(42), Some.new(42)
17 | assert_not_equal None, Some(nil)
18 | assert_equal Some(nil), Option.unit(nil)
19 | end
20 |
21 | def test_when_option_constructor_raises
22 | assert_raise(TypeError) { Option.new }
23 | end
24 |
25 | def test_monad_axioms
26 | f = lambda { |x| Option(x && x * 2) }
27 | g = lambda { |x| Option(x && x * 5) }
28 | [nil, 42].each do |value|
29 | assert_monad_axiom_1(Option, value, f)
30 | assert_monad_axiom_2(Option(value))
31 | assert_monad_axiom_3(Option(value), f, g)
32 | end
33 | end
34 |
35 | def test_when_empty_returns_none
36 | assert_equal None, Option.empty
37 | end
38 |
39 | def test_when_value_on_some_returns_value_but_on_none_raises
40 | assert_equal "foo", Some("foo").value
41 | assert_raise(NoMethodError) { None.value }
42 | end
43 |
44 | def test_when_get_on_some_returns_value_but_on_none_raises
45 | assert_equal "foo", Some("foo").get
46 | assert_raise(NoSuchElementError) { None.get }
47 | end
48 |
49 | def test_when_get_or_else_on_some_returns_value_but_on_none_returns_value_or_executes_block_or_lambda
50 | assert_equal "foo", Some("foo").get_or_else("bar")
51 | assert_equal "bar", None.get_or_else("bar")
52 | assert_equal "blk", None.get_or_else { "blk" }
53 | assert_equal "lam", None.get_or_else(lambda { "lam"} )
54 | end
55 |
56 | def test_when_or_nil_on_some_returns_value_but_on_none_returns_nil
57 | assert_equal 123, Some(123).or_nil
58 | assert_nil None.or_nil
59 | end
60 |
61 | def test_flat_map_behaves_correctly
62 | assert_equal Some("FOO"), Some("foo").flat_map { |s| Some(s.upcase) }
63 | assert_equal None, None.flat_map { |s| Some(s.upcase) }
64 | end
65 |
66 | def test_map_behaves_correctly
67 | assert_equal Some("FOO"), Some("foo").map { |s| s.upcase }
68 | assert_equal None, None.map { |s| s.upcase }
69 | end
70 |
71 | def test_shallow_flatten_behaves_correctly
72 | assert_equal Some(Some(1)), Some(Some(Some(1))).shallow_flatten
73 | assert_equal None, Some(None).shallow_flatten
74 | assert_equal Some(1), Some(1).shallow_flatten
75 | assert_equal [None, Some(1)], Some([None, Some(1)]).shallow_flatten
76 | end
77 |
78 | def test_flatten_behaves_correctly
79 | assert_equal Some(1), Some(Some(Some(1))).flatten
80 | assert_equal None, Some(None).flatten
81 | assert_equal Some(1), Some(1).flatten
82 | assert_equal [1], Some([None, Some(1)]).flatten
83 | end
84 |
85 | def test_to_s_behaves_correctly
86 | assert_equal "Some(1)", Some(1).to_s
87 | assert_equal "None", None.to_s
88 | assert_equal "Some(Some(None))", Some(Some(None)).to_s
89 | assert_equal "Some(nil)", Some(nil).to_s
90 | end
91 |
92 | def test_each_behaves_correctly
93 | vals = [None, Some(42)].inject([]) { |arr, opt| assert_nil(opt.each { |val| arr << val }); arr }
94 | assert_equal [42], vals
95 | end
96 |
97 | def test_enumerable_methods_are_available
98 | assert Some(1).all? { |v| v < 10 }
99 | assert !Some(1).all? { |v| v > 10 }
100 | assert None.all? { |v| v > 10 }
101 | end
102 |
103 | def test_to_a_behaves_correctly
104 | assert_equal [1], Some(1).to_a
105 | assert_equal [], None.to_a
106 | end
107 |
108 | def test_select_behaves_correctly
109 | assert_equal Some(1), Some(1).select { |n| n > 0 }
110 | assert_equal None, Some(1).select { |n| n < 0 }
111 | assert_equal None, None.select { |n| n < 0 }
112 | end
113 |
114 | def test_some_map_to_nil_follows_scala_behavior_returning_some_of_nil
115 | # scala> Option(1).map { x => null }
116 | # res0: Option[Null] = Some(null)
117 | assert_equal Some(nil), Option(1).map { |n| nil }
118 | end
119 | end
120 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | $:.unshift(File.dirname(File.dirname(File.expand_path(__FILE__))) + '/lib')
2 | require "rubygems"
3 | require "test/unit"
4 |
5 | # Setup code coverage via SimpleCov and post to Coveralls.io
6 | require "simplecov"
7 | require "coveralls"
8 | SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
9 | SimpleCov::Formatter::HTMLFormatter,
10 | Coveralls::SimpleCov::Formatter
11 | ]
12 | SimpleCov.start do
13 | add_filter "/test/"
14 | end
15 |
16 | require "rumonade"
17 |
18 | # see http://stackoverflow.com/questions/2709361/monad-equivalent-in-ruby
19 | module MonadAxiomTestHelpers
20 | def assert_monad_axiom_1(monad_class, value, f)
21 | assert_equal f[value], monad_class.unit(value).bind(f)
22 | end
23 |
24 | def assert_monad_axiom_2(monad)
25 | assert_equal monad, monad.bind(lambda { |v| monad.class.unit(v) })
26 | end
27 |
28 | def assert_monad_axiom_3(monad, f, g)
29 | assert_equal monad.bind(f).bind(g), monad.bind { |x| f[x].bind(g) }
30 | end
31 | end
32 |
--------------------------------------------------------------------------------