monadic 0.0.5 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
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