monadic 0.0.5 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.travis.yml ADDED
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.2
4
+ - 1.9.3
5
+ - jruby-19mode # JRuby in 1.9 mode
6
+ - rbx-19mode
7
+ notifications:
8
+ irc: "irc.freenode.org#anixe"
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.0.6
4
+ **Contains Breaking Changes**
5
+
6
+ Refactoring to use internal `Monad` class, from which all monads inherit.
7
+
8
+ Reimplemented `Maybe` as inherited class from `Monad`. The old `Option` implementation has been removed, maybe does the same though, but the code is much cleaner and obeys the 3 monadic laws.
9
+
10
+ Removed `Maybe#fetch` call with block`
11
+
12
+ `Either` and `Validation` are now in the `Monadic` namespace.
13
+
14
+ Added Travis-Ci integration, [![Build Status](https://secure.travis-ci.org/pzol/monadic.png?branch=master)](http://travis-ci.org/pzol/monadic)
15
+
16
+
3
17
  ## v0.0.5
4
18
 
5
19
  Removed the `#chain` method alias for bind in `Either`.
data/README.md CHANGED
@@ -1,25 +1,23 @@
1
1
  # Monadic
2
2
 
3
- helps dealing with exceptional situations, it comes from the sphere of functional programming and bringing the goodies I have come to love in [Scala](http://www.scala-lang.org/) to my ruby projects (hence I will be using more [Scala](http://www.scala-lang.org/) like constructs than Haskell).
3
+ helps dealing with exceptional situations, it comes from the sphere of functional programming and bringing the goodies I have come to love in [Scala](http://www.scala-lang.org/) and [Haskell](http://www.haskell.org/) to my ruby projects.
4
4
 
5
5
  My motivation to create this gem was that I often work with nested Hashes and need to reach deeply inside of them so my code is sprinkled with things like some_hash.fetch(:one, {}).fetch(:two, {}).fetch(:three, "unknown").
6
6
 
7
7
  We have the following monadics (monads, functors, applicatives and variations):
8
8
 
9
- - Option (Maybe in Haskell) - [Scala](http://www.scala-lang.org/) like with a rubyesque flavour
10
- - Either - more Haskell like
11
- - Validation
9
+ - Maybe - use if you have __one__ exception
10
+ - Either - use if you have __many__ exceptions, and one call depends on the previous
11
+ - Validation - use if you have __many__ independent calls (usually to validate an object)
12
12
 
13
13
  What's the point of using monads in ruby? To me it started with having a safe way to deal with nil objects and other exceptions.
14
- Thus you contain the erroneous behaviour within a monad - an indivisible, impenetrable unit.
15
-
16
- Monad purists might complain that there is no unit method to get the zero monad, I didn't include them, as I didn't find this idiomatic to the ruby language. I prefer to focus on the pragmatic uses of monads. If you want to learn moar about monads, see the references section at the bottom.
17
-
14
+ Thus you contain the erroneous behaviour within a monad - an indivisible, impenetrable unit. Functional programming considers _throwing_ exceptions to be a side-effect, instead we _propagate_ exceptions, i.e. return them as a result of a function call.
15
+
18
16
  A monad is most effectively described as a computation that eventually returns a value. -- Wolfgang De Meuter
19
17
 
20
18
  ## Usage
21
19
 
22
- ### Option
20
+ ### Maybe
23
21
  Is an optional type, which helps to handle error conditions gracefully. The one thing to remember about option is: 'What goes into the Option, stays in the Option'.
24
22
 
25
23
  Option(User.find(123)).name._ # ._ is a shortcut for .fetch
@@ -28,57 +26,51 @@ Is an optional type, which helps to handle error conditions gracefully. The one
28
26
  Maybe(User.find(123)).name._
29
27
 
30
28
  # confidently diving into nested hashes
31
- Maybe({})[:a][:b][:c] == None
32
- Maybe({})[:a][:b][:c].fetch('unknown') == None
29
+ Maybe({})[:a][:b][:c] == Nothing
30
+ Maybe({})[:a][:b][:c].fetch('unknown') == "unknown"
33
31
  Maybe(a: 1)[:a]._ == 1
34
32
 
35
33
  Basic usage examples:
36
34
 
37
35
  # handling nil (None serves as NullObject)
38
- obj = nil
39
- Option(obj).a.b.c == None
40
-
41
- # None stays None
42
- Option(nil)._ == "None"
43
- "#{Option(nil)}" == "None"
44
- Option(nil)._("unknown") == "unknown"
45
- Option(nil).empty? == true
46
- Option(nil).truly? == false
47
-
48
- # Some stays Some, unless you unbox it
49
- Option('FOO').downcase == Some('foo')
50
- Option('FOO').downcase.fetch == "foo"
51
- Option('FOO').downcase._ == "foo"
52
- Option('foo').empty? == false
53
- Option('foo').truly? == true
36
+ Maybe(nil).a.b.c == Nothing
37
+
38
+ # Nothing
39
+ Maybe(nil)._ == Nothing
40
+ "#{Maybe(nil)}" == "Nothing"
41
+ Maybe(nil)._("unknown") == "unknown"
42
+ Maybe(nil).empty? == true
43
+ Maybe(nil).truly? == false
44
+
45
+ # Just stays Just, unless you unbox it
46
+ Maybe('FOO').downcase == Just('foo')
47
+ Maybe('FOO').downcase.fetch == "foo" # unboxing the value
48
+ Maybe('FOO').downcase._ == "foo"
49
+ Maybe('foo').empty? == false # always non-empty
50
+ Maybe('foo').truly? == true # depends on the boxed value
51
+ Maybe(false).empty? == false
52
+ Maybe(false).truly? == false
54
53
 
55
54
  Map, select:
56
55
 
57
- Option(123).map { |value| User.find(value) } == Option(someUser) # if user found
58
- Option(0).map { |value| User.find(value) } == None # if user not found
59
- Option([1,2]).map { |value| value.to_s } == Option(["1", "2"]) # for all Enumerables
56
+ Maybe(123).map { |value| User.find(value) } == Just(someUser) # if user found
57
+ Maybe(0).map { |value| User.find(value) } == Nothing # if user not found
58
+ Maybe([1,2]).map { |value| value.to_s } == Just(["1", "2"]) # for all Enumerables
60
59
 
61
- Option('foo').select { |value| value.start_with?('f') } == Some('foo')
62
- Option('bar').select { |value| value.start_with?('f') } == None
60
+ Maybe('foo').select { |value| value.start_with?('f') } == Just('foo')
61
+ Maybe('bar').select { |value| value.start_with?('f') } == Nothing
63
62
 
64
63
  Treat it like an array:
65
64
 
66
- Option(123).to_a == [123]
67
- Option([123, 456]).to_a == [123, 456]
68
- Option(nil) == []
65
+ Maybe(123).to_a == [123]
66
+ Maybe([123, 456]).to_a == [123, 456]
67
+ Maybe(nil).to_a == []
69
68
 
70
69
  Falsey values (kind-of) examples:
71
70
 
72
- user = Option(User.find(123))
71
+ user = Maybe(User.find(123))
73
72
  user.name._
74
73
 
75
- user.fetch('You are not logged in') { |user| "You are logged in as #{user.name}" }.should == 'You are logged in as foo'
76
-
77
- if user != nil
78
- "You are logged in as foo"
79
- else
80
- "You are not logged in"
81
-
82
74
  user.subscribed? # always true
83
75
  user.subscribed?.truly? # true if subscribed is true
84
76
  user.subscribed?.fetch(false) # same as above
@@ -105,7 +97,7 @@ Slug example
105
97
 
106
98
  # do it with a default
107
99
  def slug(title)
108
- Option(title).strip.downcase.tr_s('^[a-z0-9]', '-')._('unknown-title')
100
+ Maybe(title).strip.downcase.tr_s('^[a-z0-9]', '-')._('unknown-title')
109
101
  end
110
102
 
111
103
  ### Either
@@ -244,6 +236,11 @@ Or install it yourself as:
244
236
 
245
237
  $ gem install monadic
246
238
 
239
+ ## Compatibility
240
+ Monadic is tested under ruby MRI 1.9.2, 1.9.3, jruby 1.9 mode, rbx 1.9 mode.
241
+
242
+ See the build status [![Build Status](https://secure.travis-ci.org/pzol/monadic.png?branch=master)](http://travis-ci.org/pzol/monadic)
243
+
247
244
  ## Contributing
248
245
 
249
246
  1. Fork it
data/Rakefile CHANGED
@@ -1,2 +1,11 @@
1
1
  #!/usr/bin/env rake
2
2
  require "bundler/gem_tasks"
3
+ require 'rspec/core/rake_task'
4
+
5
+ desc "Run all specs"
6
+ RSpec::Core::RakeTask.new(:spec) do |t|
7
+ t.pattern = FileList['spec/**/*_spec.rb']
8
+ t.rspec_opts = %w[--format doc --color]
9
+ end
10
+
11
+ task :default => :spec
@@ -1,93 +1,108 @@
1
- # Chain various method calls
2
- module Either
3
- def self.chain(initial=nil, &block)
4
- Either::Chain.new(&block).call(initial)
5
- end
1
+ module Monadic
2
+ # @abstract Chains function calls and stops executing if one of them fails.
3
+ class Either < Monad
4
+ def self.chain(initial=nil, &block)
5
+ Either::Chain.new(&block).call(initial)
6
+ end
6
7
 
7
- def success?
8
- is_a? Success
9
- end
8
+ def self.unit(value)
9
+ return Failure.new(value) if value.nil? || (value.respond_to?(:empty?) && value.empty?) || !value
10
+ return Success.new(value)
11
+ end
10
12
 
11
- def failure?
12
- is_a? Failure
13
- end
13
+ # Initialize is private, because it always would return an instance of Either, but Success or Failure
14
+ # are required (Either is abstract).
15
+ def initialize(value)
16
+ raise NoMethodError, "private method `new' called for #{self.class.name}, use `unit' instead"
17
+ end
14
18
 
15
- def fetch(default=@value)
16
- return default if failure?
17
- return @value
18
- end
19
- alias :_ :fetch
20
-
21
- def bind(proc=nil, &block)
22
- return self if failure?
23
-
24
- begin
25
- result = if proc && proc.arity == 0
26
- then proc.call
27
- else (proc || block).call(@value)
28
- end
29
- result ||= Failure(nil)
30
- result = Either(result) unless result.is_a? Either
31
- result
32
- rescue Exception => ex
33
- Failure(ex)
19
+ def success?
20
+ is_a? Success
34
21
  end
35
- end
36
- alias :>= :bind
37
- alias :+ :bind
38
22
 
39
- def to_s
40
- "#{self.class.name}(#{@value.nil? ? 'nil' : @value.to_s})"
41
- end
23
+ def failure?
24
+ is_a? Failure
25
+ end
42
26
 
43
- def ==(other)
44
- return false unless self.class === other
45
- return other.fetch == @value
27
+ def fetch(default=@value)
28
+ return default if failure?
29
+ return @value
30
+ end
31
+ alias :_ :fetch
32
+
33
+ def bind(proc=nil, &block)
34
+ return self if failure?
35
+
36
+ begin
37
+ result = if proc && proc.arity == 0
38
+ then proc.call
39
+ else (proc || block).call(@value)
40
+ end
41
+ result ||= Failure(nil)
42
+ result = Either(result) unless result.is_a? Either
43
+ result
44
+ rescue Exception => ex
45
+ Failure(ex)
46
+ end
47
+ end
48
+ alias :>= :bind
49
+ alias :+ :bind
46
50
  end
47
51
 
48
- end
52
+ class Either::Chain
53
+ def initialize(&block)
54
+ @chain = []
55
+ instance_eval(&block)
56
+ end
49
57
 
50
- class Either::Chain
51
- def initialize(&block)
52
- @chain = []
53
- instance_eval(&block)
54
- end
58
+ def call(initial)
59
+ @chain.inject(Success(initial)) do |result, current|
60
+ result.bind(current)
61
+ end
62
+ end
55
63
 
56
- def call(initial)
57
- @chain.inject(Success(initial)) do |result, current|
58
- result.bind(current)
64
+ def bind(proc=nil, &block)
65
+ @chain << (proc || block)
59
66
  end
60
67
  end
61
68
 
62
- def bind(proc=nil, &block)
63
- @chain << (proc || block)
69
+ # @private instance and class methods for Success and Failure
70
+ module SuccessFailure
71
+ module ClassMethods
72
+ def unit(value)
73
+ new(value)
74
+ end
75
+ end
76
+ module InstanceMethods
77
+ def initialize(value)
78
+ @value = join(value)
79
+ end
80
+ end
64
81
  end
65
82
 
66
- end
67
-
68
- class Success
69
- include Either
70
- def initialize(value)
71
- @value = value
83
+ class Success < Either
84
+ extend SuccessFailure::ClassMethods
85
+ include SuccessFailure::InstanceMethods
72
86
  end
73
- end
74
87
 
75
- class Failure
76
- include Either
77
- def initialize(value)
78
- @value = value
88
+ class Failure < Either
89
+ extend SuccessFailure::ClassMethods
90
+ include SuccessFailure::InstanceMethods
79
91
  end
80
- end
81
92
 
82
- def Success(value)
83
- Success.new(value)
84
- end
93
+ def Failure(value)
94
+ Failure.new(value)
95
+ end
85
96
 
86
- def Failure(value)
87
- Failure.new(value)
88
- end
97
+ # Factory method
98
+ # @return [Success]
99
+ def Success(value)
100
+ Success.new(value)
101
+ end
89
102
 
90
- def Either(value)
91
- return Failure(value) if value.nil? || (value.respond_to?(:empty?) && value.empty?) || !value
92
- return Success(value)
103
+ # Magic factory
104
+ # @return [Success, Failure] depending whether +value+ is falsey
105
+ def Either(value)
106
+ Either.unit(value)
107
+ end
93
108
  end
@@ -0,0 +1,92 @@
1
+ module Monadic
2
+ # @api private helps treating `Maybe` like Either in Scala
3
+ module ScalaStuff
4
+ def map(proc = nil, &block)
5
+ return Maybe(@value.map(&block)) if @value.is_a?(Enumerable)
6
+ return Maybe((proc || block).call(@value))
7
+ end
8
+
9
+ def select(proc = nil, &block)
10
+ return Maybe(@value.select(&block)) if @value.is_a?(Enumerable)
11
+ return Nothing unless (proc || block).call(@value)
12
+ return self
13
+ end
14
+ end
15
+
16
+ class Maybe < Monad
17
+ include ScalaStuff
18
+
19
+ def self.unit(value)
20
+ return Nothing if value.nil? || (value.respond_to?(:empty?) && value.empty?)
21
+ return Just.new(value)
22
+ end
23
+
24
+ # Initialize is private, because it always would return an instance of Maybe, but Just or Nothing
25
+ # are required (Maybe is abstract).
26
+ def initialize(*args)
27
+ raise NoMethodError, "private method `new' called for #{self.class.name}, use `unit' instead"
28
+ end
29
+
30
+ def empty?
31
+ @value.respond_to?(:empty?) && @value.empty?
32
+ end
33
+
34
+ def to_ary
35
+ return [@value].flatten if @value.respond_to? :flatten
36
+ return [@value]
37
+ end
38
+ alias :to_a :to_ary
39
+
40
+ def truly?
41
+ @value == true
42
+ end
43
+ end
44
+
45
+ class Just < Maybe
46
+ def initialize(value)
47
+ @value = join(value)
48
+ end
49
+
50
+ def fetch(default=nil)
51
+ @value
52
+ end
53
+ alias :_ :fetch
54
+
55
+ def method_missing(m, *args)
56
+ Maybe(@value.__send__(m, *args))
57
+ end
58
+ end
59
+
60
+ class Nothing < Maybe
61
+ class << self
62
+ def fetch(default=nil)
63
+ return self if default.nil?
64
+ return default
65
+ end
66
+ alias :_ :fetch
67
+
68
+ def method_missing(m, *args)
69
+ self
70
+ end
71
+
72
+ def to_ary
73
+ []
74
+ end
75
+ alias :to_a :to_ary
76
+
77
+ def to_s
78
+ 'Nothing'
79
+ end
80
+
81
+ def truly?
82
+ false
83
+ end
84
+ end
85
+ end
86
+
87
+ def Maybe(value)
88
+ Maybe.unit(value)
89
+ end
90
+ alias :Just :Maybe
91
+ alias :Nothing :Maybe
92
+ end
@@ -0,0 +1,38 @@
1
+ module Monadic
2
+ class Monad
3
+ def self.unit(value)
4
+ new(value)
5
+ end
6
+
7
+ def initialize(value)
8
+ @value = join(value)
9
+ end
10
+
11
+ def bind(proc=nil, &block)
12
+ (proc || block).call(@value)
13
+ end
14
+
15
+ # If the passed value is monad already, get the value to avoid nesting
16
+ # M[M[A]] is equivalent to M[A]
17
+ def join(value)
18
+ if value.is_a? self.class then value.fetch
19
+ else value end
20
+ end
21
+
22
+ # Unwraps the the Monad
23
+ def fetch
24
+ @value
25
+ end
26
+ alias :_ :fetch
27
+
28
+ def to_s
29
+ pretty_class_name = self.class.name.split('::')[-1]
30
+ "#{pretty_class_name}(#{@value.nil? ? 'nil' : @value.to_s})"
31
+ end
32
+
33
+ def ==(other)
34
+ return false unless other.is_a? self.class
35
+ @value == other.instance_variable_get(:@value)
36
+ end
37
+ end
38
+ end
@@ -1,24 +1,28 @@
1
- def Validation(&block)
2
- Validation.new.call(&block)
3
- end
4
-
5
- # Conducts several function calls which do checks, of which each must return Success or Failure
6
- # and returns a list of all failures.
7
- # Validation is not a monad, but an deemed an applicative functor
8
- class Validation
9
- def initialize
10
- @result = Success([])
1
+ module Monadic
2
+ # Wraps the construction of the Validation class
3
+ def Validation(&block)
4
+ Validation.new.call(&block)
11
5
  end
12
6
 
13
- def call(&block)
14
- instance_eval(&block)
15
- end
7
+ # Conducts several function calls which do checks, of which each must return Success or Failure
8
+ # and returns a list of all failures.
9
+ # Validation is not a monad, but an deemed an applicative functor
10
+ class Validation
11
+ def initialize
12
+ @result = Success([])
13
+ end
14
+
15
+ def call(&block)
16
+ instance_eval(&block)
17
+ end
16
18
 
17
- def check(proc=nil, &block)
18
- result = (proc || block).call
19
- raise NotEitherError, "Expected #{result.inspect} to be an Either" unless result.is_a? Either
20
- @result = Failure(@result.fetch << result.fetch) if result.failure?
19
+ #
20
+ def check(proc=nil, &block)
21
+ result = (proc || block).call
22
+ raise NotEitherError, "Expected #{result.inspect} to be an Either" unless result.is_a? Either
23
+ @result = Failure(@result.fetch << result.fetch) if result.failure?
21
24
 
22
- @result
25
+ @result
26
+ end
23
27
  end
24
28
  end
@@ -1,3 +1,3 @@
1
1
  module Monadic
2
- VERSION = "0.0.5"
2
+ VERSION = "0.0.6"
3
3
  end
data/lib/monadic.rb CHANGED
@@ -1,7 +1,8 @@
1
1
  require 'monadic/version'
2
2
 
3
3
  require 'monadic/errors'
4
- require 'monadic/option'
4
+ require 'monadic/monad'
5
+ require 'monadic/maybe'
5
6
  require 'monadic/either'
6
7
  require 'monadic/validation'
7
8
 
@@ -9,5 +10,3 @@ module Monadic
9
10
  end
10
11
 
11
12
  include Monadic
12
- None = Monadic::None
13
- Some = Monadic::Some
data/monadic.gemspec CHANGED
@@ -4,7 +4,7 @@ require File.expand_path('../lib/monadic/version', __FILE__)
4
4
  Gem::Specification.new do |gem|
5
5
  gem.authors = ["Piotr Zolnierek"]
6
6
  gem.email = ["pz@anixe.pl"]
7
- gem.description = %q{brings some functional goodness to ruby}
7
+ gem.description = %q{brings some functional goodness to ruby by giving you some monads}
8
8
  gem.summary = %q{see README}
9
9
  gem.homepage = "http://github.com/pzol/monadic"
10
10
 
@@ -21,4 +21,5 @@ Gem::Specification.new do |gem|
21
21
  gem.add_development_dependency 'guard-bundler'
22
22
  gem.add_development_dependency 'growl'
23
23
  gem.add_development_dependency 'activesupport'
24
+ gem.add_development_dependency 'rake'
24
25
  end
data/spec/either_spec.rb CHANGED
@@ -1,14 +1,34 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe 'Either' do
3
+ describe Monadic::Either do
4
+ it 'Either cannot be created using #new, use #unit instead' do
5
+ expect { Either.new(1) }.to raise_error NoMethodError
6
+ end
7
+
8
+ it 'Success.new and Success.unit and Success() return the same' do
9
+ Success(1).should == Success.unit(1)
10
+ Success.new(1).should == Success.unit(1)
11
+
12
+ Success(nil).should == Success.unit(nil)
13
+ Success.new(nil).should == Success.unit(nil)
14
+ end
15
+
16
+ it 'Failure.new and Failure.unit and Failure() return the same' do
17
+ Failure(1).should == Failure.unit(1)
18
+ Failure.new(1).should == Failure.unit(1)
19
+
20
+ Failure(nil).should == Failure.unit(nil)
21
+ Failure.new(nil).should == Failure.unit(nil)
22
+ end
23
+
4
24
  it 'Success and Failure should be kind of Either' do
5
- Success.new(0).should be_kind_of(Either)
6
- Failure.new(0).should be_kind_of(Either)
25
+ Success.unit(0).should be_kind_of(Either)
26
+ Failure.unit(0).should be_kind_of(Either)
7
27
  end
8
28
 
9
29
  it '#to_s works' do
10
- Success.new("it worked!").to_s.should == "Success(it worked!)"
11
- Failure.new(nil).to_s.should == "Failure(nil)"
30
+ Success.unit("it worked!").to_s.should == "Success(it worked!)"
31
+ Failure.unit(nil).to_s.should == "Failure(nil)"
12
32
  end
13
33
 
14
34
  it 'allows to verify equality' do
@@ -190,7 +210,7 @@ describe 'Either' do
190
210
  bind {|path| load_file(path) }.
191
211
  bind {|content| process_content(content) }
192
212
  either.should be_a_failure
193
- KeyError.should === either.fetch
213
+ either.fetch.should be_a KeyError
194
214
  end
195
215
 
196
216
  it 'instance variables' do
@@ -0,0 +1,3 @@
1
+ if defined?(JRUBY_VERSION) && RUBY_VERSION =~ /^1\.9/
2
+ KeyError = IndexError
3
+ end
@@ -0,0 +1,145 @@
1
+ require 'spec_helper'
2
+
3
+ describe Monadic::Maybe do
4
+ it_behaves_like 'a Monad' do
5
+ let(:monad) { Maybe }
6
+ end
7
+
8
+ it 'Maybe cannot be created using #new, use #unit instead' do
9
+ expect { Maybe.new(1) }.to raise_error NoMethodError
10
+ end
11
+
12
+ it 'nil as value always returns Nothing()' do
13
+ Maybe(nil).a.b.c.should == Nothing
14
+ end
15
+
16
+ it 'on non-existant methods, returns Nothing' do
17
+ Maybe({}).a.b.c.should == Nothing
18
+ end
19
+
20
+ describe Monadic::Nothing do
21
+ it_behaves_like 'a Monad' do
22
+ let(:monad) { Nothing }
23
+ end
24
+
25
+ it 'Nothing stays Nothing' do
26
+ Maybe(nil).fetch.should == Nothing
27
+ Maybe(nil)._.should == Nothing
28
+ Maybe(nil).empty?.should be_true
29
+ end
30
+
31
+ it 'Nothing#to_s is "Nothing"' do
32
+ option = Maybe(nil)
33
+ "#{option}".should == "Nothing"
34
+ Nothing.to_s.should == "Nothing"
35
+ end
36
+
37
+ it 'Nothing is always empty' do
38
+ Nothing.empty?.should be_true
39
+ Maybe(nil).empty?.should be_true
40
+ end
41
+
42
+ it '[] as value always returns Nothing()' do
43
+ Maybe([]).a.should == Nothing
44
+ end
45
+
46
+ it 'is always empty and false' do
47
+ Nothing.empty?.should be_true
48
+ Nothing.truly?.should be_false
49
+ end
50
+ end
51
+
52
+ describe Monadic::Just do
53
+ it_behaves_like 'a Monad' do
54
+ let(:monad) { Just }
55
+ end
56
+
57
+ it 'Just stays Just' do
58
+ Maybe('foo').should be_kind_of(Just)
59
+ Maybe('foo').empty?.should be_false
60
+ end
61
+
62
+ it 'Just#to_s is "Just(value)"' do
63
+ Just.unit(123).to_s.should == "Just(123)"
64
+ end
65
+ end
66
+
67
+ it 'calling methods on Maybe always returns an Maybe with the transformed value' do
68
+ Maybe('FOO').downcase.should == Just('foo')
69
+ end
70
+
71
+ it '#fetch returns the value of an option' do
72
+ Maybe('foo').fetch.should == 'foo'
73
+ Maybe('foo')._.should == 'foo'
74
+ end
75
+
76
+ it 'returns the value of an option with a default, in case value is Nothing' do
77
+ Maybe(nil).fetch('bar').should == 'bar'
78
+ Maybe(nil)._('bar').should == 'bar'
79
+ end
80
+
81
+ it 'returns the value and not the default if it is Just' do
82
+ Maybe('FOO').downcase.fetch('bar').should == 'foo'
83
+ Maybe('FOO').downcase._('bar').should == 'foo'
84
+ end
85
+
86
+ it 'is never falsey' do
87
+ Maybe('foo').should_not be_false
88
+ Maybe(nil).should_not be_false
89
+ Maybe(false).truly?.should be_false
90
+ Maybe(true).truly?.should be_true
91
+ Maybe(nil).truly?.should be_false
92
+ end
93
+
94
+ it 'handles (kind-of) falsey values' do
95
+ FalseyUser = Struct.new(:name, :subscribed)
96
+ user = Maybe(FalseyUser.new(name = 'foo', subscribed = true))
97
+ user.subscribed.fetch(false).should be_true
98
+ user.subscribed.truly?.should be_true
99
+
100
+ user = Maybe(nil)
101
+ user.subscribed.fetch(false).should be_false
102
+ user.subscribed.truly?.should be_false
103
+ end
104
+
105
+ it 'allows to use map' do
106
+ Maybe(nil).map { |e| Hash.new(:key => e) }.should == Nothing
107
+ Maybe('foo').map { |e| Hash.new(:key => e) }.should == Just(Hash.new(:key => 'foo'))
108
+ Maybe([1,2]).map { |e| e.to_s }.should == Just(["1", "2"])
109
+ end
110
+
111
+ it 'allows to use select' do
112
+ Maybe('foo').select { |e| e.start_with?('f') }.should == Just('foo')
113
+ Maybe('bar').select { |e| e.start_with?('f') }.should == Nothing
114
+ Maybe(nil).select { |e| e.never_called }.should == Nothing
115
+ Maybe([1, 2]).select { |e| e == 1 }.should == Just([1])
116
+ end
117
+
118
+ it 'acts as an array' do
119
+ Maybe('foo').to_a.should == ['foo']
120
+ Maybe(['foo', 'bar']).to_a.should == ['foo', 'bar']
121
+ Maybe(nil).to_a.should == []
122
+ end
123
+
124
+ it 'diving into hashes' do
125
+ Maybe({})['a']['b']['c'].should == Nothing
126
+ Maybe({a: 1})[:a]._.should == 1
127
+ end
128
+
129
+ it 'should support Rumonades example' do
130
+ require 'active_support/time'
131
+ def format_date_in_march(time_or_date_or_nil)
132
+ Maybe(time_or_date_or_nil). # wraps possibly-nil value in an Maybe monad (Some or Nothing)
133
+ map(&:to_date). # transforms a contained Time value into a Date value
134
+ select {|d| d.month == 3}. # filters out non-matching Date values (Some becomes Nothing)
135
+ map(&:to_s). # transforms a contained Date value into a String value
136
+ map {|s| s.gsub('-', '')}. # transforms a contained String value by removing '-'
137
+ fetch("not in march!") # returns the contained value, or the alternative if Nothing
138
+ end
139
+
140
+ format_date_in_march(nil).should == "not in march!"
141
+ format_date_in_march(Time.parse('2009-01-01 01:02')).should == "not in march!"
142
+ format_date_in_march(Time.parse('2011-03-21 12:34')).should == "20110321"
143
+ end
144
+
145
+ end
@@ -0,0 +1,37 @@
1
+ require 'spec_helper'
2
+
3
+ shared_examples 'a Monad' do
4
+ describe 'axioms' do
5
+ it '1st monadic law: left-identity' do
6
+ f = ->(value) { monad.unit(value + 1) }
7
+ monad::unit(1).bind do |value|
8
+ f.(value)
9
+ end.should == f.(1)
10
+ end
11
+
12
+ it '2nd monadic law: right-identy - unit and bind do not change the value' do
13
+ monad.unit(1).bind do |value|
14
+ monad.unit(value)
15
+ end.should == monad.unit(1)
16
+ end
17
+
18
+ it '3rd monadic law: associativity' do
19
+ f = ->(value) { monad.unit(value + 1) }
20
+ g = ->(value) { monad.unit(value + 100) }
21
+
22
+ id1 = monad.unit(1).bind do |a|
23
+ f.(a)
24
+ end.bind do |b|
25
+ g.(b)
26
+ end
27
+
28
+ id2 = monad.unit(1).bind do |a|
29
+ f.(a).bind do |b|
30
+ g.(b)
31
+ end
32
+ end
33
+
34
+ id1.should == id2
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,12 @@
1
+ require 'spec_helper'
2
+
3
+ describe Monadic::Monad do
4
+ it_behaves_like 'a Monad' do
5
+ let(:monad) { Monad }
6
+ end
7
+
8
+ it '#to_s shows the monad name and its value' do
9
+ Monad.unit(1).to_s.should == 'Monad(1)'
10
+ Monad.unit(nil).to_s.should == 'Monad(nil)'
11
+ end
12
+ end
data/spec/spec_helper.rb CHANGED
@@ -3,3 +3,6 @@ Bundler.require(:default, :test)
3
3
 
4
4
  $LOAD_PATH << File.expand_path('../../lib', __FILE__)
5
5
  require File.expand_path('../../lib/monadic', __FILE__)
6
+
7
+ require 'monad_axioms'
8
+ require 'jruby_fixes'
@@ -1,6 +1,6 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe 'Validation' do
3
+ describe Monadic::Validation do
4
4
  Person = Struct.new(:age, :gender, :sobriety, :name)
5
5
 
6
6
  it 'a projection of Success and Failure' do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: monadic
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
4
+ version: 0.0.6
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-05-02 00:00:00.000000000 Z
12
+ date: 2012-05-03 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rspec
@@ -107,7 +107,23 @@ dependencies:
107
107
  - - ! '>='
108
108
  - !ruby/object:Gem::Version
109
109
  version: '0'
110
- description: brings some functional goodness to ruby
110
+ - !ruby/object:Gem::Dependency
111
+ name: rake
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ description: brings some functional goodness to ruby by giving you some monads
111
127
  email:
112
128
  - pz@anixe.pl
113
129
  executables: []
@@ -116,6 +132,7 @@ extra_rdoc_files: []
116
132
  files:
117
133
  - .gitignore
118
134
  - .rspec
135
+ - .travis.yml
119
136
  - CHANGELOG.md
120
137
  - Gemfile
121
138
  - Guardfile
@@ -125,12 +142,16 @@ files:
125
142
  - lib/monadic.rb
126
143
  - lib/monadic/either.rb
127
144
  - lib/monadic/errors.rb
128
- - lib/monadic/option.rb
145
+ - lib/monadic/maybe.rb
146
+ - lib/monadic/monad.rb
129
147
  - lib/monadic/validation.rb
130
148
  - lib/monadic/version.rb
131
149
  - monadic.gemspec
132
150
  - spec/either_spec.rb
133
- - spec/option_spec.rb
151
+ - spec/jruby_fixes.rb
152
+ - spec/maybe_spec.rb
153
+ - spec/monad_axioms.rb
154
+ - spec/monad_spec.rb
134
155
  - spec/spec_helper.rb
135
156
  - spec/validation_spec.rb
136
157
  homepage: http://github.com/pzol/monadic
@@ -159,6 +180,10 @@ specification_version: 3
159
180
  summary: see README
160
181
  test_files:
161
182
  - spec/either_spec.rb
162
- - spec/option_spec.rb
183
+ - spec/jruby_fixes.rb
184
+ - spec/maybe_spec.rb
185
+ - spec/monad_axioms.rb
186
+ - spec/monad_spec.rb
163
187
  - spec/spec_helper.rb
164
188
  - spec/validation_spec.rb
189
+ has_rdoc:
@@ -1,101 +0,0 @@
1
- require 'singleton'
2
- # Represents optional values. Instances of Option are either an instance of Some or the object None.
3
- #
4
-
5
- # Helper function which returns Some or None respectively, depending on their value
6
- # I find this moar simplistic in ruby than the traditional #bind and #unit
7
- def Option(value)
8
- return Monadic::None if value.nil? || (value.respond_to?(:empty?) && value.empty?)
9
- return Monadic::Some.new(value)
10
- end
11
- alias :Some :Option
12
- alias :Maybe :Option
13
-
14
-
15
- # Represents the Option if there is some value available
16
- module Monadic
17
- class Some
18
- def initialize(value)
19
- @value = value
20
- end
21
-
22
- def to_ary
23
- return [@value].flatten if @value.respond_to? :flatten
24
- return [@value]
25
- end
26
- alias :to_a :to_ary
27
-
28
- def empty?
29
- false
30
- end
31
-
32
- def truly?
33
- @value == true
34
- end
35
-
36
- def fetch(default=None, &block)
37
- return block.call(@value) if block_given?
38
- return @value
39
- end
40
- alias :or :fetch
41
- alias :_ :fetch
42
-
43
- def map(func = nil, &block)
44
- return Option(@value.map(&block)) if @value.is_a?(Enumerable)
45
- return Option((func || block).call(@value))
46
- end
47
-
48
- def method_missing(m, *args)
49
- Option(@value.__send__(m, *args))
50
- end
51
-
52
- def select(func = nil, &block)
53
- return Option(@value.select(&block)) if @value.is_a?(Enumerable)
54
- return None unless (func || block).call(@value)
55
- return self
56
- end
57
-
58
- def to_s
59
- "Some(#{@value.to_s})"
60
- end
61
-
62
- def ==(other)
63
- return false unless other.is_a? Some
64
- @value == other.instance_variable_get(:@value)
65
- end
66
- end
67
-
68
- # Represents the Option if there is no value available
69
- class None
70
- class << self
71
- def to_ary
72
- []
73
- end
74
- alias :to_a :to_ary
75
-
76
- def empty?
77
- true
78
- end
79
-
80
- def fetch(default=nil)
81
- raise NoValueError if default.nil?
82
- default
83
- end
84
- alias :or :fetch
85
- alias :_ :fetch
86
-
87
- def method_missing(m, *args)
88
- self
89
- end
90
-
91
- def to_s
92
- 'None'
93
- end
94
-
95
- def truly?
96
- false
97
- end
98
-
99
- end
100
- end
101
- end
data/spec/option_spec.rb DELETED
@@ -1,129 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe 'Option' do
4
- it 'nil as value always returns None()' do
5
- Option(nil).a.b.c.should == None
6
- end
7
-
8
- it 'None stays None' do
9
- expect { Option(nil)._ }.to raise_error Monadic::NoValueError
10
- Option(nil).empty?.should be_true
11
- end
12
-
13
- it 'Some stays Some' do
14
- Option('foo').should be_kind_of(Some)
15
- Option('foo').empty?.should be_false
16
- end
17
-
18
- it 'Some#to_s is "Some(value)"' do
19
- Some(123).to_s.should == "Some(123)"
20
- end
21
-
22
- it 'None#to_s is "None"' do
23
- option = Option(nil)
24
- "#{option}".should == "None"
25
- end
26
-
27
- it 'None is always empty' do
28
- None.empty?.should be_true
29
- Maybe(nil).empty?.should be_true
30
- end
31
-
32
- it '[] as value always returns None()' do
33
- Option([]).a.should == None
34
- end
35
-
36
- it 'calling methods on Option always returns an Option with the transformed value' do
37
- Option('FOO').downcase.should == Some('foo')
38
- end
39
-
40
- it '#fetch returns the value of an option' do
41
- Option('foo').fetch.should == 'foo'
42
- Option('foo')._.should == 'foo'
43
- end
44
-
45
- it 'returns the value of an option with a default, in case value is None' do
46
- Option(nil).fetch('bar').should == 'bar'
47
- Option(nil)._('bar').should == 'bar'
48
- end
49
-
50
- it 'returns the value and not the default if it is Some' do
51
- Option('FOO').downcase.fetch('bar').should == 'foo'
52
- Option('FOO').downcase._('bar').should == 'foo'
53
- end
54
-
55
- it 'returns the value applied to a block if it is Some' do
56
- Option('foo').fetch('bar') { |val| "You are logged in as #{val}" }.should == 'You are logged in as foo'
57
- Option(nil).fetch('You are not logged in') { |val| "You are logged in as #{val}" }.should == 'You are not logged in'
58
- end
59
-
60
- it 'is never falsey' do
61
- Option('foo').should_not be_false
62
- Option(nil).should_not be_false
63
- end
64
-
65
- class User
66
- attr_reader :name
67
- def initialize(name)
68
- @name = name
69
- end
70
- def subscribed?
71
- true
72
- end
73
- end
74
-
75
- it 'allows to use a block with fetch and _' do
76
- user = Option(User.new('foo'))
77
- user.fetch('You are not logged in') { |user| "You are logged in as #{user.name}" }.should == 'You are logged in as foo'
78
- end
79
-
80
- it 'handles (kind-of) falsey values' do
81
- user = Option(User.new('foo'))
82
- user.subscribed?.or(false).should be_true
83
- user.subscribed?.truly?.should be_true
84
-
85
- user = Option(nil)
86
- user.subscribed?.or(false).should be_false
87
- user.subscribed?.truly?.should be_false
88
- end
89
-
90
- it 'allows to use map' do
91
- Option(nil).map { |e| Hash.new(:key => e) }.should == None
92
- Option('foo').map { |e| Hash.new(:key => e) }.should == Some(Hash.new(:key => 'foo'))
93
- Option([1,2]).map { |e| e.to_s }.should == Some(["1", "2"])
94
- end
95
-
96
- it 'allows to use select' do
97
- Option('foo').select { |e| e.start_with?('f') }.should == Some('foo')
98
- Option('bar').select { |e| e.start_with?('f') }.should == None
99
- Option(nil).select { |e| e.never_called }.should == None
100
- Option([1, 2]).select { |e| e == 1 }.should == Some([1])
101
- end
102
-
103
- it 'acts as an array' do
104
- Option('foo').to_a.should == ['foo']
105
- Option(['foo', 'bar']).to_a.should == ['foo', 'bar']
106
- Option(nil).to_a.should == []
107
- end
108
-
109
- it 'diving into hashes' do
110
- Maybe({})['a']['b']['c'].should == None
111
- Maybe({a: 1})[:a]._.should == 1
112
- end
113
-
114
- it 'should support Rumonades example' do
115
- require 'active_support/time'
116
- def format_date_in_march(time_or_date_or_nil)
117
- Option(time_or_date_or_nil). # wraps possibly-nil value in an Option monad (Some or None)
118
- map(&:to_date). # transforms a contained Time value into a Date value
119
- select {|d| d.month == 3}. # filters out non-matching Date values (Some becomes None)
120
- map(&:to_s). # transforms a contained Date value into a String value
121
- map {|s| s.gsub('-', '')}. # transforms a contained String value by removing '-'
122
- fetch("not in march!") # returns the contained value, or the alternative if None
123
- end
124
-
125
- format_date_in_march(nil).should == "not in march!"
126
- format_date_in_march(Time.parse('2009-01-01 01:02')).should == "not in march!"
127
- format_date_in_march(Time.parse('2011-03-21 12:34')).should == "20110321"
128
- end
129
- end