├── .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 | [![Gem Version](https://img.shields.io/gem/v/rumonade.svg)](https://rubygems.org/gems/rumonade) 16 | [![Build Status](https://travis-ci.org/ms-ati/rumonade.png)](https://travis-ci.org/ms-ati/rumonade) 17 | [![Dependency Status](https://gemnasium.com/ms-ati/rumonade.png)](https://gemnasium.com/ms-ati/rumonade) 18 | [![Code Climate](https://codeclimate.com/github/ms-ati/rumonade.png)](https://codeclimate.com/github/ms-ati/rumonade) 19 | [![Coverage Status](https://coveralls.io/repos/ms-ati/rumonade/badge.png)](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 | --------------------------------------------------------------------------------