├── .gitignore ├── .travis.yml ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── lib ├── ribimaybe.rb └── version.rb ├── maybe.gif ├── ribimaybe.gemspec └── spec ├── applicative_functor_spec.rb ├── bug_spec.rb ├── functor_spec.rb ├── maybe_spec.rb ├── monad_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 1.9.3 4 | script: 5 | - bundle exec rake 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | gemspec 3 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 unsymbol 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ribimaybe 2 | 3 | > "Flavouring the ocean with a teaspoon of sugar." 4 | 5 | [![Gem Version](https://badge.fury.io/rb/ribimaybe.svg)](http://badge.fury.io/rb/ribimaybe) 6 | [![Travis](https://travis-ci.org/mcfilib/ribimaybe.svg?branch=master)](http://travis-ci.org/mcfilib/ribimaybe) 7 | 8 | ![](maybe.gif) 9 | 10 | A tiny Ruby library that provides a Maybe datatype which is a Functor, 11 | Applicative Functor and Monad. 12 | 13 | ## Installation 14 | 15 | Add this line to your application's Gemfile: 16 | 17 | gem 'ribimaybe' 18 | 19 | And then execute: 20 | 21 | $ bundle 22 | 23 | Or install it yourself as: 24 | 25 | $ gem install ribimaybe 26 | 27 | ## Usage 28 | 29 | This is a small library and so it doesn't offer lots of creature comforts. The 30 | one escape hatch it does offer is the ability to convert `nil` into `Nothing`. 31 | 32 | ``` ruby 33 | include Ribimaybe::Maybe 34 | 35 | Maybe(42) # => Just(42) 36 | Maybe(nil) # => Nothing 37 | ``` 38 | 39 | And that's it, once you have lifted your value into a `Maybe` you can treat it 40 | as a `Functor`, `Applicative Functor` or `Monad`. If you want to pull your value 41 | out of a `Maybe`, we got you covered too. 42 | 43 | ``` ruby 44 | Just(42).maybe(false) { |x| x == 42 } # => true 45 | Nothing.maybe(false) { |x| x == 42 } # => false 46 | ``` 47 | 48 | ### Functor [\[info\]](http://learnyouahaskell.com/functors-applicative-functors-and-monoids) 49 | 50 | ``` ruby 51 | include Ribimaybe::Maybe 52 | 53 | # Apply functions within Maybe and retain structure. 54 | Just(42).map { |x| x * x } # => Just(1764) 55 | Nothing.map { |x| x * x } # => Nothing 56 | ``` 57 | 58 | ### Applicative Functor [\[info\]](http://learnyouahaskell.com/functors-applicative-functors-and-monoids) 59 | 60 | ``` ruby 61 | include Ribimaybe::Maybe 62 | 63 | # Wrap functions inside functors and apply them to other functors! 64 | Just do |x, y| 65 | x * y 66 | end.apply(pure(42)).apply(pure(42)) # => Just(1764) 67 | 68 | Just do |x| 69 | x * x 70 | end.apply(Nothing) # => Nothing 71 | 72 | # We can't define <*> but we can define a different operator with the same 73 | # semantics! 74 | Just { |x, y| x * y } >> pure(42) >> pure(42) # => Just(1764) 75 | ``` 76 | 77 | ### Monad [\[info\]](http://www.learnyouahaskell.com/a-fistful-of-monads) 78 | 79 | ``` ruby 80 | include Ribimaybe::Maybe 81 | 82 | # Chain together computations and pretend you're a Haskeller. 83 | Just(42).bind do |x| 84 | unit(x - 21).bind do |y| 85 | if x * x > 100 then unit(x) else unit(y) end 86 | end 87 | end # => Just(42) 88 | 89 | # You guessed it! If have Nothing, you get Nothing. 90 | Nothing.bind do |x| 91 | unit(x * x) 92 | end # => Nothing 93 | 94 | # We even have >>= but it's called >= and you you need to pass a Proc or a 95 | # lambda. 96 | Just(42) >= -> (x) do 97 | unit(x - 21) >= -> (y) do 98 | if x * x > 100 then unit(x) else unit(y) end 99 | end 100 | end # => Just(42) 101 | ``` 102 | 103 | ## Contributing 104 | 105 | 1. Fork it 106 | 2. Create your feature branch (`git checkout -b my-new-feature`) 107 | 3. Commit your changes (`git commit -am 'Add some feature'`) 108 | 4. Push to the branch (`git push origin my-new-feature`) 109 | 5. Create new Pull Request 110 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core" 3 | require "rspec/core/rake_task" 4 | RSpec::Core::RakeTask.new(:default) do |spec| 5 | spec.pattern = FileList["spec/**/*_spec.rb"] 6 | end 7 | -------------------------------------------------------------------------------- /lib/ribimaybe.rb: -------------------------------------------------------------------------------- 1 | require "contracts" 2 | 3 | module Ribimaybe 4 | module Maybe 5 | include Contracts 6 | 7 | # Ensure constants are available to Ruby contracts. 8 | module Nothing; end 9 | class Just; end 10 | 11 | module Nothing 12 | include Contracts 13 | 14 | # Nothing string representation. 15 | def self.to_s 16 | "Nothing" 17 | end 18 | 19 | alias_method :inspect, :to_s 20 | 21 | # Compares a Nothing to another Maybe. 22 | # 23 | # ==== Attributes 24 | # 25 | # * +other+ - The other Maybe value. 26 | Contract Or[Nothing, Just] => Bool 27 | def self.===(other) 28 | self == other 29 | end 30 | 31 | # No operation. Always returns the default value. 32 | Contract Any, Proc => Any 33 | def self.maybe(default, &_) 34 | default 35 | end 36 | 37 | # No operation. Always returns Nothing. 38 | Contract Proc => Nothing 39 | def self.map(&_) 40 | self 41 | end 42 | 43 | # No operation. Always returns Nothing. 44 | Contract Any => Nothing 45 | def self.apply(_) 46 | self 47 | end 48 | 49 | class << self 50 | alias_method :>>, :apply 51 | end 52 | 53 | # No operation. Always returns Nothing. 54 | Contract Proc => Nothing 55 | def self.bind(fn = nil, &_) 56 | self 57 | end 58 | 59 | class << self 60 | alias_method :>=, :bind 61 | end 62 | end 63 | 64 | class Just 65 | include Contracts 66 | 67 | def initialize(value) 68 | @value = value 69 | end 70 | 71 | # Just string representation. 72 | def to_s 73 | "Just(#{@value.inspect})" 74 | end 75 | 76 | alias_method :inspect, :to_s 77 | 78 | # Compares a Just to another Maybe. 79 | # 80 | # ==== Attributes 81 | # 82 | # * +other+ - The other Maybe value. 83 | # 84 | # ==== Examples 85 | # 86 | # Just(1) == Just(1) # => true 87 | # Just(1) == Just(2) # => false 88 | # Just(1) == Nothing # => false 89 | Contract Or[Nothing, Just] => Bool 90 | def ==(other) 91 | other.maybe(false) do |value| 92 | @value == value 93 | end 94 | end 95 | 96 | # Fetches a value out of a Just and returns the application 97 | # of fn to the internal value. 98 | # 99 | # ==== Attributes 100 | # 101 | # * +_+ - Default value that's never used. 102 | # * +fn+ - Function to be applied to value inside Just. 103 | # 104 | # ==== Examples 105 | # 106 | # Just(1).maybe(false) { |x| x == 1 } # => true 107 | # Just(1).maybe(42) { |x| x } # => 1 108 | Contract Any, Proc => Any 109 | def maybe(_, &fn) 110 | fn.curry.(@value) 111 | end 112 | 113 | # Applies fn to a value inside Just and re-wraps it in another Just. 114 | # 115 | # ==== Attributes 116 | # 117 | # * +fn+ - Function to be applied inside Just. 118 | # 119 | # ==== Examples 120 | # 121 | # Just(1).map { |x| x + 1 } # => Just(2) 122 | # Just { |x, y| x + y }.map { |f| f.(1) } # => Just(#) 123 | Contract Proc => Just 124 | def map(&fn) 125 | Just.new(fn.curry.(@value)) 126 | end 127 | 128 | # Applies fn inside Just to a value in a Just and re-wraps the result in 129 | # a Just. Note that functions are curried by default. 130 | # 131 | # ==== Attributes 132 | # 133 | # * +value+ - Maybe value. 134 | # 135 | # ==== Examples 136 | # 137 | # Just do |x| 138 | # x + x 139 | # end.apply(Just(1)) # => Just(2) 140 | Contract Or[Nothing, Just] => Or[Nothing, Just] 141 | def apply(value) 142 | value.map { |v| @value.curry.(v) } 143 | end 144 | 145 | alias_method :>>, :apply 146 | 147 | # Applies fn to value inside Just (note contract constraint). 148 | # 149 | # ==== Attributes 150 | # 151 | # * +fn+ - Function to be applied inside our Just. 152 | # 153 | # ==== Examples 154 | # 155 | # Just(1).bind do |x| 156 | # unit(x + x) 157 | # end # => Just(2) 158 | Contract Proc => Or[Nothing, Just] 159 | def bind(fn = nil, &block) 160 | (fn || block).curry.(@value) 161 | end 162 | 163 | alias_method :>=, :bind 164 | end 165 | 166 | # Converts nil to Nothing or lifts value into a Just. Accepts a optional 167 | # block or value. 168 | # 169 | # ==== Attributes 170 | # 171 | # * +value+ - Value to be wrapped. 172 | # * +fn+ - Block or function to be wrapped. 173 | # 174 | # ==== Examples 175 | # 176 | # Maybe(nil) # => Nothing 177 | # Maybe(1) # => Just(1) 178 | # Maybe { |x| x } # => Just(#) 179 | Contract Any, Or[nil, Proc] => Or[Nothing, Just] 180 | def Maybe(value = nil, &fn) 181 | (value || fn) ? Just.new(value || fn.curry) : Nothing 182 | end 183 | 184 | alias_method :Just, :Maybe 185 | alias_method :pure, :Maybe 186 | alias_method :unit, :Maybe 187 | end 188 | end 189 | -------------------------------------------------------------------------------- /lib/version.rb: -------------------------------------------------------------------------------- 1 | module Ribimaybe 2 | VERSION = "0.1.0" 3 | end 4 | -------------------------------------------------------------------------------- /maybe.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcfilib/ribimaybe/e3a70ebe6dcb2285efa32ddf06ef82e8925437a6/maybe.gif -------------------------------------------------------------------------------- /ribimaybe.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path("../lib", __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require "version" 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "ribimaybe" 8 | spec.version = Ribimaybe::VERSION 9 | spec.required_ruby_version = ">= 1.9.3" 10 | spec.authors = ["unsymbol"] 11 | spec.email = ["hello@philipcunningham.org"] 12 | spec.description = "Maybe Functor, Applicative and Monad" 13 | spec.summary = "A tiny Ruby library that provides a Maybe datatype which is a Functor, Applicative Functor and Monad instance." 14 | spec.homepage = "https://github.com/unsymbol/ribimaybe" 15 | spec.license = "MIT" 16 | 17 | spec.files = `git ls-files`.split($/) 18 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 19 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 20 | spec.require_paths = ["lib"] 21 | 22 | spec.add_development_dependency "bundler", "~> 1.3" 23 | spec.add_development_dependency "pry" 24 | spec.add_development_dependency "rake" 25 | spec.add_development_dependency "rspec" 26 | spec.add_runtime_dependency "contracts", "~> 0.4" 27 | end 28 | -------------------------------------------------------------------------------- /spec/applicative_functor_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | include Ribimaybe::Maybe 3 | describe "Applicative Instance" do 4 | let(:ap) do 5 | ->(f, a){ f.(a) }.curry 6 | end 7 | 8 | let(:id) do 9 | ->(x){ x } 10 | end 11 | 12 | let(:dot) do 13 | ->(f, g){ ->(x){ f.(g.(x)) } } 14 | end 15 | 16 | let(:f) do 17 | ->(x){ ->(y) { x } }.(SecureRandom.base64(1000)) 18 | end 19 | 20 | let(:g) do 21 | ->(x){ ->(y) { x } }.(SecureRandom.base64(1000)) 22 | end 23 | 24 | [:apply, :>>].each do |m| 25 | # pure id <*> v = v 26 | describe "identity" do 27 | context "when i have nothing" do 28 | it do 29 | expect(pure(&id).public_send(m, Nothing)).to eq(Nothing) 30 | end 31 | end 32 | 33 | context "when i have just :x" do 34 | it do 35 | expect(pure(&id).public_send(m, pure(:x))).to eq(pure(:x)) 36 | end 37 | end 38 | end 39 | 40 | # pure (.) <*> u <*> v <*> w = u <*> (v <*> w) 41 | describe "composition" do 42 | context "when i have nothing" do 43 | it do 44 | lhs = pure(&dot).public_send(m, pure(&f)).public_send(m, pure(&g)).public_send(m, Nothing) 45 | rhs = pure(&f).public_send(m, pure(&g).public_send(m, Nothing)) 46 | expect(lhs).to eq(rhs) 47 | end 48 | end 49 | 50 | context "when i have just :x" do 51 | it do 52 | lhs = pure(&dot).public_send(m, pure(&f)).public_send(m, pure(&g)).public_send(m, pure(:x)) 53 | rhs = pure(&f).public_send(m, pure(&g).public_send(m, pure(:x))) 54 | expect(lhs).to eq(rhs) 55 | end 56 | end 57 | end 58 | 59 | # pure f <*> pure x = pure (f x) 60 | describe "homomorphism" do 61 | context "when i have nothing" do 62 | it do 63 | expect(pure(&f).public_send(m, Nothing)).to eq(Nothing) 64 | end 65 | end 66 | 67 | context "when i have just :x" do 68 | it do 69 | expect(pure(&f).public_send(m, pure(:x))).to eq(pure(f.(:x))) 70 | end 71 | end 72 | end 73 | 74 | # u <*> pure y = pure ($ y) <*> u 75 | describe "interchange" do 76 | context "when i have nothing" do 77 | it do 78 | expect(pure(&f).public_send(m, Nothing)).to eq(pure(&ap.(f)).public_send(m, Nothing)) 79 | end 80 | end 81 | 82 | context "when i have just :x" do 83 | it do 84 | expect(pure(&f).public_send(m, pure(:x))).to eq(pure(&ap.(f)).public_send(m, pure(:x))) 85 | end 86 | end 87 | end 88 | 89 | # fmap f x = pure f <*> x 90 | describe "map" do 91 | context "when i have nothing" do 92 | it do 93 | expect(Nothing.map(&f)).to eq(pure(&f).public_send(m, Nothing)) 94 | end 95 | end 96 | 97 | context "when i have just :x" do 98 | it do 99 | expect(Just(:x).map(&f)).to eq(pure(&f).public_send(m, Just(:x))) 100 | end 101 | end 102 | end 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /spec/bug_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | include Ribimaybe::Maybe 3 | describe "Bugs" do 4 | describe "issue17" do 5 | it do 6 | result = Just(1).bind{ |x| 7 | Nothing.bind{ |y| 8 | rturn(x + y) 9 | } 10 | } 11 | expect(result).to eq(Nothing) 12 | end 13 | end 14 | 15 | describe "issue19" do 16 | context "when i have nothing" do 17 | it do 18 | x = Nothing 19 | result = case x 20 | when Just 21 | x.maybe(:x) { |x| x } 22 | when Nothing 23 | :nothing 24 | end 25 | expect(result).to eq(:nothing) 26 | end 27 | end 28 | 29 | context "when i have just :x" do 30 | it do 31 | x = Just(:x) 32 | result = case x 33 | when Just 34 | x.maybe(:x) { |x| x } 35 | when Nothing 36 | :nothing 37 | end 38 | expect(result).to eq(:x) 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/functor_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | include Ribimaybe::Maybe 3 | describe "Functor Instance" do 4 | let(:id) do 5 | ->(x){ x } 6 | end 7 | 8 | let(:f) do 9 | ->(x){ ->(y) { x } }.(SecureRandom.base64(1000)).extend(Composable) 10 | end 11 | 12 | let(:g) do 13 | ->(x){ ->(y) { x } }.(SecureRandom.base64(1000)).extend(Composable) 14 | end 15 | 16 | # fmap id = id 17 | describe "identity" do 18 | context "when i have nothing" do 19 | it do 20 | expect(Nothing.map(&id)).to eq(Nothing) 21 | end 22 | end 23 | 24 | context "when i have just :x" do 25 | it do 26 | expect(Just(:x).map(&id)).to eq(Just(:x)) 27 | end 28 | end 29 | end 30 | 31 | # fmap (f . g) = fmap f . fmap g 32 | describe "composition" do 33 | context "when i have nothing" do 34 | it do 35 | expect(Nothing.map(&(f * g))).to eq(Nothing.map(&g).map(&f)) 36 | end 37 | end 38 | 39 | context "when i have just :x" do 40 | it do 41 | expect(Just(:x).map(&(f * g))).to eq(Just(:x).map(&g).map(&f)) 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/maybe_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | include Ribimaybe::Maybe 3 | describe Ribimaybe::Maybe do 4 | describe ".maybe" do 5 | context "when i have nothing" do 6 | it "should give me back a default" do 7 | expect(Nothing.maybe(false) { |_| true }).to eq(false) 8 | end 9 | end 10 | end 11 | 12 | describe "#maybe" do 13 | context "when i have something" do 14 | it "should give me back something" do 15 | expect(Just(:x).maybe(:y) { |x| x }).to eq(:x) 16 | end 17 | end 18 | end 19 | 20 | describe "#Maybe()" do 21 | context "when i have nil" do 22 | it "should give me back nothing" do 23 | expect(Maybe(nil)).to eq(Nothing) 24 | end 25 | end 26 | 27 | context "when i have :x" do 28 | it "should give me back just :x" do 29 | expect(Maybe(:x)).to eq(Just(:x)) 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/monad_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | include Ribimaybe::Maybe 3 | describe "Monad Instance" do 4 | let(:id) do 5 | ->(x) { x } 6 | end 7 | 8 | let(:lifted_id) do 9 | ->(x) { id.(unit(x)) } 10 | end 11 | 12 | let(:f) do 13 | ->(x){ ->(y) { unit(x) } }.(SecureRandom.base64(1000)) 14 | end 15 | 16 | let(:g) do 17 | ->(x){ ->(y) { unit(x) } }.(SecureRandom.base64(1000)) 18 | end 19 | 20 | [:bind, :>=].each do |m| 21 | # return a >>= f = f a 22 | describe "left identity" do 23 | context "when i have nothing" do 24 | it do 25 | expect(Nothing.public_send(m, &lifted_id)).to eq(Nothing) 26 | end 27 | end 28 | 29 | context "when i have just :x" do 30 | it do 31 | expect(unit(:x).public_send(m, &lifted_id)).to eq(lifted_id.(:x)) 32 | end 33 | end 34 | end 35 | 36 | # m >>= return = m 37 | describe "right identity" do 38 | context "when i have nothing" do 39 | it do 40 | expect(Nothing.public_send(m, &lifted_id)).to eq(Nothing) 41 | end 42 | end 43 | 44 | context "when i have just :x" do 45 | it do 46 | expect(Just(:x).public_send(m, &lifted_id)).to eq(Just(:x)) 47 | end 48 | end 49 | end 50 | 51 | # (m >>= f) >>= g = m >>= (\x -> f x >>= g) 52 | describe "associativity" do 53 | context "when i have nothing" do 54 | it do 55 | lhs = Nothing.public_send(m, &f).public_send(m, &g) 56 | rhs = Nothing.bind { |x| f.(x).public_send(m, &g) } 57 | expect(lhs).to eq(rhs) 58 | end 59 | end 60 | 61 | context "when i have just :x" do 62 | it do 63 | lhs = Just(:x).public_send(m, &f).public_send(m, &g) 64 | rhs = Just(:x).bind { |x| f.(x).public_send(m, &g) } 65 | expect(lhs).to eq(rhs) 66 | end 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + "/../lib") 2 | 3 | require "bundler/setup" 4 | require "pry" 5 | require "ribimaybe" 6 | require "rubygems" 7 | 8 | RSpec.configure do |config| 9 | end 10 | 11 | module Composable 12 | def compose(f, g) 13 | ->(x){f.(g.(x))} 14 | end 15 | 16 | def *(g) 17 | compose(self, g) 18 | end 19 | end 20 | --------------------------------------------------------------------------------