monadic 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +16 -0
- data/README.md +65 -10
- data/lib/monadic/either.rb +97 -0
- data/lib/monadic/errors.rb +4 -0
- data/lib/monadic/option.rb +73 -65
- data/lib/monadic/version.rb +1 -1
- data/lib/monadic.rb +9 -2
- data/spec/either_spec.rb +194 -0
- data/spec/option_spec.rb +11 -13
- data/spec/spec_helper.rb +1 -0
- metadata +6 -2
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,19 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## v0.0.4
|
4
|
+
|
5
|
+
To be more idiomatic rubyuesque, rename `#value` to `#fetch`, which throws now an `NoValueError`.
|
6
|
+
Thanks to [@pithyless](https://twitter.com/#!/pithyless) for the suggestion.
|
7
|
+
|
8
|
+
It now supports the Either monad, e.g.
|
9
|
+
|
10
|
+
either = Success(0).
|
11
|
+
bind { Success(1) }.
|
12
|
+
bind { Failure(2) }.
|
13
|
+
bind { Success(3) }
|
14
|
+
|
15
|
+
either == Failure(2) # the third bind is NOT executed
|
16
|
+
|
3
17
|
## v0.0.3
|
4
18
|
|
5
19
|
`Some#map` and `Some#select` accept proc and block, you can now use:
|
@@ -9,3 +23,5 @@
|
|
9
23
|
Option("FOO").downcase # old
|
10
24
|
|
11
25
|
Removed `#none?`, please use `#empty?` instead.
|
26
|
+
|
27
|
+
`Some` and `None` are now in the `Monadic` namespace, however they are aliased when requiring `monadic`
|
data/README.md
CHANGED
@@ -2,12 +2,12 @@
|
|
2
2
|
|
3
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 to my ruby projects (hence I will be using more Scala like constructs than Haskell).
|
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:
|
8
8
|
|
9
|
-
- Option (Maybe in Haskell)
|
10
|
-
- Either
|
9
|
+
- Option (Maybe in Haskell) - Scala like with a rubyesque flavour
|
10
|
+
- Either - more Haskell like
|
11
11
|
|
12
12
|
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.
|
13
13
|
Thus you contain the erroneous behaviour within a monad - an indivisible, impenetrable unit.
|
@@ -17,15 +17,14 @@ Thus you contain the erroneous behaviour within a monad - an indivisible, impene
|
|
17
17
|
### Option
|
18
18
|
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'.
|
19
19
|
|
20
|
-
|
21
|
-
Option(User.find(123)).name._ # ._ is a shortcut for .value
|
20
|
+
Option(User.find(123)).name._ # ._ is a shortcut for .fetch
|
22
21
|
|
23
22
|
# if you prefer the alias Maybe instead of option
|
24
23
|
Maybe(User.find(123)).name._
|
25
24
|
|
26
25
|
# confidently diving into nested hashes
|
27
26
|
Maybe({})[:a][:b][:c] == None
|
28
|
-
Maybe({})[:a][:b][:c].
|
27
|
+
Maybe({})[:a][:b][:c].fetch('unknown') == None
|
29
28
|
Maybe(a: 1)[:a]._ == 1
|
30
29
|
|
31
30
|
Basic usage examples:
|
@@ -43,7 +42,7 @@ Basic usage examples:
|
|
43
42
|
|
44
43
|
# Some stays Some, unless you unbox it
|
45
44
|
Option('FOO').downcase == Some('foo')
|
46
|
-
Option('FOO').downcase.
|
45
|
+
Option('FOO').downcase.fetch == "foo"
|
47
46
|
Option('FOO').downcase._ == "foo"
|
48
47
|
Option('foo').empty? == false
|
49
48
|
Option('foo').truly? == true
|
@@ -68,7 +67,7 @@ Falsey values (kind-of) examples:
|
|
68
67
|
user = Option(User.find(123))
|
69
68
|
user.name._
|
70
69
|
|
71
|
-
user.
|
70
|
+
user.fetch('You are not logged in') { |user| "You are logged in as #{user.name}" }.should == 'You are logged in as foo'
|
72
71
|
|
73
72
|
if user != nil
|
74
73
|
"You are logged in as foo"
|
@@ -77,7 +76,7 @@ Falsey values (kind-of) examples:
|
|
77
76
|
|
78
77
|
user.subscribed? # always true
|
79
78
|
user.subscribed?.truly? # true if subscribed is true
|
80
|
-
user.subscribed?.
|
79
|
+
user.subscribed?.fetch(false) # same as above
|
81
80
|
user.subscribed?.or(false) # same as above
|
82
81
|
|
83
82
|
Remember! an Option is never false (in Ruby terms), if you want to know if it is false, call `#empty?` of `#truly?`
|
@@ -104,9 +103,65 @@ Slug example
|
|
104
103
|
Option(title).strip.downcase.tr_s('^[a-z0-9]', '-')._('unknown-title')
|
105
104
|
end
|
106
105
|
|
106
|
+
### Either
|
107
|
+
Its main purpose here to handle errors gracefully, by chaining multiple calls in a functional way and stop evaluating them as soon as the first fails.
|
108
|
+
Assume you need several calls to construct some object in order to be useful, after each you need to check for success. Also you want to catch exceptions and not let them bubble upwards.
|
109
|
+
|
110
|
+
`Success` represents a successfull execution of an operation (Right in Scala, Haskell).
|
111
|
+
`Failure` represents a failure to execute an operation (Left in Scala, Haskell).
|
112
|
+
|
113
|
+
The `Either()` wrapper will treat `nil`, `false` or `empty?` as a `Failure` and all others as `Success`.
|
114
|
+
|
115
|
+
result = parse_and_validate_params(params).
|
116
|
+
bind ->(user_id) { User.find(user_id) }. # if #find returns null it will become a Failure
|
117
|
+
bind ->(user) { authorized?(user); user }. # if authorized? raises an Exception, it will be a Failure
|
118
|
+
bind ->(user) { UserDecorator(user) }
|
119
|
+
|
120
|
+
if result.success?
|
121
|
+
@user = result.fetch # result.fetch or result._ contains the
|
122
|
+
render 'page'
|
123
|
+
else
|
124
|
+
@error = result.fetch
|
125
|
+
render 'error_page'
|
126
|
+
end
|
127
|
+
|
128
|
+
You can use alternate syntaxes to achieve the same goal:
|
129
|
+
|
130
|
+
# block and Haskell like >= operator
|
131
|
+
Either(operation).
|
132
|
+
>= { successful_method }.
|
133
|
+
>= { failful_operation }
|
134
|
+
|
135
|
+
# start with a Success, for instance a parameter
|
136
|
+
Success('pzol').
|
137
|
+
bind ->(previous) { good }.
|
138
|
+
bind -> { bad }
|
139
|
+
|
140
|
+
Either.chain do
|
141
|
+
bind -> { good } # >= is not supported for Either.chain, only bind
|
142
|
+
bind -> { better } # better returns Success(some_int)
|
143
|
+
bind ->(previous_result) { previous_result + 1 }
|
144
|
+
end
|
145
|
+
|
146
|
+
either = Either(something)
|
147
|
+
either += truth? Success('truth, only the truth') : Failure('lies, damn lies')
|
148
|
+
|
149
|
+
Exceptions are wrapped into a Failure:
|
150
|
+
|
151
|
+
Either(true).
|
152
|
+
bind -> { fail 'get me out of here' } # return a Failure(RuntimeError)
|
153
|
+
|
154
|
+
Another example:
|
155
|
+
|
156
|
+
Success(params).
|
157
|
+
bind ->(params) { Either(params.fetch(:path)) }
|
158
|
+
bind ->(path) { load_stuff(params) }
|
159
|
+
|
107
160
|
|
108
|
-
|
161
|
+
## References
|
109
162
|
|
163
|
+
* [Wikipedia Monad](See also http://en.wikipedia.org/wiki/Monad)
|
164
|
+
* [Learn You a Haskell - for a few monads more](http://learnyouahaskell.com/for-a-few-monads-more)
|
110
165
|
* [Monad equivalend in Ruby](http://stackoverflow.com/questions/2709361/monad-equivalent-in-ruby)
|
111
166
|
* [Option Type ](http://devblog.avdi.org/2011/05/30/null-objects-and-falsiness/)
|
112
167
|
* [NullObject and Falsiness by @avdi](http://devblog.avdi.org/2011/05/30/null-objects-and-falsiness/)
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# Chain various method calls
|
2
|
+
module Either
|
3
|
+
class NoEitherError < StandardError; end
|
4
|
+
|
5
|
+
def self.chain(initial=nil, &block)
|
6
|
+
Either::Chain.new(&block).call(initial)
|
7
|
+
end
|
8
|
+
|
9
|
+
def success?
|
10
|
+
is_a? Success
|
11
|
+
end
|
12
|
+
|
13
|
+
def failure?
|
14
|
+
is_a? Failure
|
15
|
+
end
|
16
|
+
|
17
|
+
def fetch(default=@value)
|
18
|
+
return default if failure?
|
19
|
+
return @value
|
20
|
+
end
|
21
|
+
alias :_ :fetch
|
22
|
+
|
23
|
+
def bind(proc=nil, &block)
|
24
|
+
return self if failure?
|
25
|
+
|
26
|
+
begin
|
27
|
+
result = if proc && proc.arity == 0
|
28
|
+
then proc.call
|
29
|
+
else (proc || block).call(@value)
|
30
|
+
end
|
31
|
+
result ||= Failure(nil)
|
32
|
+
result = Either(result) unless result.is_a? Either
|
33
|
+
result
|
34
|
+
rescue Exception => ex
|
35
|
+
Failure(ex)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
alias :>= :bind
|
39
|
+
alias :+ :bind
|
40
|
+
alias :chain :bind
|
41
|
+
|
42
|
+
def to_s
|
43
|
+
"#{self.class.name}(#{@value.nil? ? 'nil' : @value.to_s})"
|
44
|
+
end
|
45
|
+
|
46
|
+
def ==(other)
|
47
|
+
return false unless self.class === other
|
48
|
+
return other.fetch == @value
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
class Either::Chain
|
54
|
+
def initialize(&block)
|
55
|
+
@chain = []
|
56
|
+
instance_eval(&block)
|
57
|
+
end
|
58
|
+
|
59
|
+
def call(initial)
|
60
|
+
@chain.inject(Success(initial)) do |result, current|
|
61
|
+
result.bind(current)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def bind(proc=nil, &block)
|
66
|
+
@chain << (proc || block)
|
67
|
+
end
|
68
|
+
alias :chain :bind
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
class Success
|
73
|
+
include Either
|
74
|
+
def initialize(value)
|
75
|
+
@value = value
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
class Failure
|
80
|
+
include Either
|
81
|
+
def initialize(value)
|
82
|
+
@value = value
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def Success(value)
|
87
|
+
Success.new(value)
|
88
|
+
end
|
89
|
+
|
90
|
+
def Failure(value)
|
91
|
+
Failure.new(value)
|
92
|
+
end
|
93
|
+
|
94
|
+
def Either(value)
|
95
|
+
return Failure(value) if value.nil? || (value.respond_to?(:empty?) && value.empty?) || !value
|
96
|
+
return Success(value)
|
97
|
+
end
|
data/lib/monadic/option.rb
CHANGED
@@ -5,89 +5,97 @@ require 'singleton'
|
|
5
5
|
# Helper function which returns Some or None respectively, depending on their value
|
6
6
|
# I find this moar simplistic in ruby than the traditional #bind and #unit
|
7
7
|
def Option(value)
|
8
|
-
return None if value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
9
|
-
return Some.new(value)
|
8
|
+
return Monadic::None if value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
9
|
+
return Monadic::Some.new(value)
|
10
10
|
end
|
11
11
|
alias :Some :Option
|
12
12
|
alias :Maybe :Option
|
13
13
|
|
14
|
-
# Represents the Option if there is some value available
|
15
|
-
class Some
|
16
|
-
def initialize(value)
|
17
|
-
@value = value
|
18
|
-
end
|
19
|
-
|
20
|
-
def to_ary
|
21
|
-
return [@value].flatten if @value.respond_to? :flatten
|
22
|
-
return [@value]
|
23
|
-
end
|
24
|
-
alias :to_a :to_ary
|
25
|
-
|
26
|
-
def empty?
|
27
|
-
false
|
28
|
-
end
|
29
|
-
|
30
|
-
def truly?
|
31
|
-
@value == true
|
32
|
-
end
|
33
|
-
|
34
|
-
def value(default=None, &block)
|
35
|
-
return default if empty?
|
36
|
-
return block.call(@value) if block_given?
|
37
|
-
return @value
|
38
|
-
end
|
39
|
-
alias :or :value
|
40
|
-
alias :_ :value
|
41
14
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
Option(@value.__send__(m, *args))
|
49
|
-
end
|
50
|
-
|
51
|
-
def select(func = nil, &block)
|
52
|
-
return Option(@value.select(&block)) if @value.is_a?(Enumerable)
|
53
|
-
return None unless (func || block).call(@value)
|
54
|
-
return self
|
55
|
-
end
|
56
|
-
|
57
|
-
def ==(other)
|
58
|
-
return false unless other.is_a? Some
|
59
|
-
@value == other.instance_variable_get(:@value)
|
60
|
-
end
|
61
|
-
|
62
|
-
def to_s
|
63
|
-
"Some(#{@value.to_s})"
|
64
|
-
end
|
65
|
-
end
|
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
|
66
21
|
|
67
|
-
# Represents the Option if there is no value available
|
68
|
-
class None
|
69
|
-
class << self
|
70
22
|
def to_ary
|
71
|
-
[]
|
23
|
+
return [@value].flatten if @value.respond_to? :flatten
|
24
|
+
return [@value]
|
72
25
|
end
|
73
26
|
alias :to_a :to_ary
|
74
27
|
|
75
28
|
def empty?
|
76
|
-
|
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))
|
77
46
|
end
|
78
47
|
|
79
48
|
def method_missing(m, *args)
|
80
|
-
|
49
|
+
Option(@value.__send__(m, *args))
|
81
50
|
end
|
82
51
|
|
83
|
-
def
|
84
|
-
|
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
|
85
56
|
end
|
86
57
|
|
87
|
-
def
|
88
|
-
|
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
|
+
|
89
99
|
end
|
90
|
-
alias :or :value
|
91
|
-
alias :_ :value
|
92
100
|
end
|
93
101
|
end
|
data/lib/monadic/version.rb
CHANGED
data/lib/monadic.rb
CHANGED
@@ -1,5 +1,12 @@
|
|
1
1
|
require 'monadic/version'
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
require 'monadic/errors'
|
5
4
|
require 'monadic/option'
|
5
|
+
require 'monadic/either'
|
6
|
+
|
7
|
+
module Monadic
|
8
|
+
end
|
9
|
+
|
10
|
+
include Monadic
|
11
|
+
None = Monadic::None
|
12
|
+
Some = Monadic::Some
|
data/spec/either_spec.rb
ADDED
@@ -0,0 +1,194 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Either' do
|
4
|
+
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)
|
7
|
+
end
|
8
|
+
|
9
|
+
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)"
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'allows to verify equality' do
|
15
|
+
Success(1).should == Success(1)
|
16
|
+
Success(2).should_not == Success(1)
|
17
|
+
Success(1).should_not == Failure(1)
|
18
|
+
Failure(1).should == Failure(1)
|
19
|
+
Failure(1).should_not == Failure(2)
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'wraps a nil result to Failure' do
|
23
|
+
Success(nil).bind { nil }.should == Failure(nil)
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'catches exceptions and returns them as Failure' do
|
27
|
+
either = Success(nil).
|
28
|
+
bind { raise 'error' }
|
29
|
+
|
30
|
+
either.should be_kind_of Failure
|
31
|
+
error = either.fetch
|
32
|
+
error.should be_kind_of RuntimeError
|
33
|
+
error.message.should == 'error'
|
34
|
+
end
|
35
|
+
|
36
|
+
class User
|
37
|
+
def self.find(id)
|
38
|
+
case id
|
39
|
+
when -1; raise 'invalid user id'
|
40
|
+
when 0; nil
|
41
|
+
else User.new(id)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
attr_reader :name
|
46
|
+
def initialize(id)
|
47
|
+
@name = "User #{id}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'Either(nil || false) returns a Failure' do
|
52
|
+
Either(nil).should == Failure(nil)
|
53
|
+
Either(false).should == Failure(false)
|
54
|
+
Failure.should === Either(nil)
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'Either with a non-falsey value returns success' do
|
58
|
+
Success.should === Either(1)
|
59
|
+
Success.should === Either(true)
|
60
|
+
Success.should === Either("string")
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'works' do
|
64
|
+
either = Either(true).
|
65
|
+
chain -> { User.find(-1) }
|
66
|
+
either.failure?.should be_true
|
67
|
+
RuntimeError.should === either.fetch
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'does NOT call subsequent binds on Failure and returns the first Failure in the end' do
|
71
|
+
either = Success(0).
|
72
|
+
bind { Failure(1) }.
|
73
|
+
bind { Failure(2) }.
|
74
|
+
bind { Success(3) }
|
75
|
+
|
76
|
+
either.should == Failure(1)
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'returns the last Success' do
|
80
|
+
either = Success(nil).
|
81
|
+
bind { Success(1) }.
|
82
|
+
bind { Success(2) }
|
83
|
+
|
84
|
+
either.should == Success(2)
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'allows to #fetch the value of the Success or Failure' do
|
88
|
+
Failure(1).fetch.should == 1
|
89
|
+
Success(2).fetch.should == 2
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'allows to #fetch the value with a default if it failed' do
|
93
|
+
Failure(1).fetch(2).should == 2
|
94
|
+
Success(1).fetch(2).should == 1
|
95
|
+
end
|
96
|
+
|
97
|
+
it 'works with pattern matching (kind of)' do
|
98
|
+
either = Success('ok')
|
99
|
+
|
100
|
+
matched = case either
|
101
|
+
when Success; "yeah: #{either.fetch}"
|
102
|
+
when Failure; "oh no: #{either.fetch}"
|
103
|
+
end
|
104
|
+
|
105
|
+
matched.should == "yeah: ok"
|
106
|
+
end
|
107
|
+
|
108
|
+
it 'allows Haskell like syntax' do
|
109
|
+
either = Success(1).
|
110
|
+
>= { Success(2) }
|
111
|
+
|
112
|
+
either.should == Success(2)
|
113
|
+
end
|
114
|
+
|
115
|
+
it 'passes the result value from the previous call to the next' do
|
116
|
+
either = Success(1).
|
117
|
+
>= {|prev| Success(prev + 1) }. # a block
|
118
|
+
>= -> prev { Success(prev + 100) } # lambda/proc
|
119
|
+
|
120
|
+
either.should == Success(102)
|
121
|
+
end
|
122
|
+
|
123
|
+
it 'returns the boxed value with the #_ alias for #fetch' do
|
124
|
+
Failure(99)._.should == 99
|
125
|
+
end
|
126
|
+
|
127
|
+
it 'allows you to use parameterless lambdas (#arity == 0)' do
|
128
|
+
(Success(0) >=
|
129
|
+
-> { Success(1) }
|
130
|
+
).should == Success(1)
|
131
|
+
end
|
132
|
+
|
133
|
+
it 'allows you to use lambdas with the + operator' do
|
134
|
+
either = Success(0) +
|
135
|
+
-> { Success(1) } +
|
136
|
+
-> { Success(2) }
|
137
|
+
|
138
|
+
either.should == Success(2)
|
139
|
+
end
|
140
|
+
|
141
|
+
it 'allows you to use +=' do
|
142
|
+
either = Success(0)
|
143
|
+
either += -> { Success(1) }
|
144
|
+
|
145
|
+
either.should == Success(1)
|
146
|
+
end
|
147
|
+
|
148
|
+
it 'allows to check whether the result was a success or failure' do
|
149
|
+
Success(0).success?.should be_true
|
150
|
+
Success(1).failure?.should be_false
|
151
|
+
|
152
|
+
Failure(2).success?.should be_false
|
153
|
+
Failure(3).failure?.should be_true
|
154
|
+
end
|
155
|
+
|
156
|
+
it 'supprts Either.chain' do
|
157
|
+
Either.chain do
|
158
|
+
bind -> { Success(1) }
|
159
|
+
bind -> { Success(2) }
|
160
|
+
end.should == Success(2)
|
161
|
+
|
162
|
+
Either.chain do
|
163
|
+
bind -> { Success(1) }
|
164
|
+
bind ->(p) { Success(p + 1) }
|
165
|
+
end.should == Success(2)
|
166
|
+
|
167
|
+
Either.chain do
|
168
|
+
bind { Success(1) }
|
169
|
+
end.should == Success(1)
|
170
|
+
end
|
171
|
+
|
172
|
+
it 'README example' do
|
173
|
+
params = { :path => 'foo' }
|
174
|
+
def load_file(path);
|
175
|
+
fail "invalid path" unless path
|
176
|
+
Success("bar");
|
177
|
+
end
|
178
|
+
def process_content(content); content.start_with?('b') ? Success(content.upcase) : Failure('invalid content'); end
|
179
|
+
|
180
|
+
either = Either(true).
|
181
|
+
bind { params.fetch(:path) }.
|
182
|
+
bind {|path| load_file(path) }.
|
183
|
+
bind {|content| process_content(content) }
|
184
|
+
either.should be_a_success
|
185
|
+
either.fetch.should == 'BAR'
|
186
|
+
|
187
|
+
either = Either(true).
|
188
|
+
bind { {}.fetch(:path) }.
|
189
|
+
bind {|path| load_file(path) }.
|
190
|
+
bind {|content| process_content(content) }
|
191
|
+
either.should be_a_failure
|
192
|
+
KeyError.should === either.fetch
|
193
|
+
end
|
194
|
+
end
|
data/spec/option_spec.rb
CHANGED
@@ -6,7 +6,7 @@ describe 'Option' do
|
|
6
6
|
end
|
7
7
|
|
8
8
|
it 'None stays None' do
|
9
|
-
Option(nil)._.
|
9
|
+
expect { Option(nil)._ }.to raise_error Monadic::NoValueError
|
10
10
|
Option(nil).empty?.should be_true
|
11
11
|
end
|
12
12
|
|
@@ -22,7 +22,6 @@ describe 'Option' do
|
|
22
22
|
it 'None#to_s is "None"' do
|
23
23
|
option = Option(nil)
|
24
24
|
"#{option}".should == "None"
|
25
|
-
"#{option._}".should == "None"
|
26
25
|
end
|
27
26
|
|
28
27
|
it 'None is always empty' do
|
@@ -38,24 +37,24 @@ describe 'Option' do
|
|
38
37
|
Option('FOO').downcase.should == Some('foo')
|
39
38
|
end
|
40
39
|
|
41
|
-
it 'returns the value of an option' do
|
42
|
-
Option('foo').
|
40
|
+
it '#fetch returns the value of an option' do
|
41
|
+
Option('foo').fetch.should == 'foo'
|
43
42
|
Option('foo')._.should == 'foo'
|
44
43
|
end
|
45
44
|
|
46
45
|
it 'returns the value of an option with a default, in case value is None' do
|
47
|
-
Option(nil).
|
46
|
+
Option(nil).fetch('bar').should == 'bar'
|
48
47
|
Option(nil)._('bar').should == 'bar'
|
49
48
|
end
|
50
49
|
|
51
50
|
it 'returns the value and not the default if it is Some' do
|
52
|
-
Option('FOO').downcase.
|
51
|
+
Option('FOO').downcase.fetch('bar').should == 'foo'
|
53
52
|
Option('FOO').downcase._('bar').should == 'foo'
|
54
53
|
end
|
55
54
|
|
56
55
|
it 'returns the value applied to a block if it is Some' do
|
57
|
-
Option('foo').
|
58
|
-
Option(nil).
|
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'
|
59
58
|
end
|
60
59
|
|
61
60
|
it 'is never falsey' do
|
@@ -73,9 +72,9 @@ describe 'Option' do
|
|
73
72
|
end
|
74
73
|
end
|
75
74
|
|
76
|
-
it 'allows to use a block with
|
75
|
+
it 'allows to use a block with fetch and _' do
|
77
76
|
user = Option(User.new('foo'))
|
78
|
-
user.
|
77
|
+
user.fetch('You are not logged in') { |user| "You are logged in as #{user.name}" }.should == 'You are logged in as foo'
|
79
78
|
end
|
80
79
|
|
81
80
|
it 'handles (kind-of) falsey values' do
|
@@ -120,12 +119,11 @@ describe 'Option' do
|
|
120
119
|
select {|d| d.month == 3}. # filters out non-matching Date values (Some becomes None)
|
121
120
|
map(&:to_s). # transforms a contained Date value into a String value
|
122
121
|
map {|s| s.gsub('-', '')}. # transforms a contained String value by removing '-'
|
123
|
-
|
122
|
+
fetch("not in march!") # returns the contained value, or the alternative if None
|
124
123
|
end
|
125
124
|
|
126
125
|
format_date_in_march(nil).should == "not in march!"
|
127
126
|
format_date_in_march(Time.parse('2009-01-01 01:02')).should == "not in march!"
|
128
127
|
format_date_in_march(Time.parse('2011-03-21 12:34')).should == "20110321"
|
129
|
-
end
|
130
|
-
|
128
|
+
end
|
131
129
|
end
|
data/spec/spec_helper.rb
CHANGED
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.
|
4
|
+
version: 0.0.4
|
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-
|
12
|
+
date: 2012-05-01 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rspec
|
@@ -123,9 +123,12 @@ files:
|
|
123
123
|
- README.md
|
124
124
|
- Rakefile
|
125
125
|
- lib/monadic.rb
|
126
|
+
- lib/monadic/either.rb
|
127
|
+
- lib/monadic/errors.rb
|
126
128
|
- lib/monadic/option.rb
|
127
129
|
- lib/monadic/version.rb
|
128
130
|
- monadic.gemspec
|
131
|
+
- spec/either_spec.rb
|
129
132
|
- spec/option_spec.rb
|
130
133
|
- spec/spec_helper.rb
|
131
134
|
homepage: http://github.com/pzol/monadic
|
@@ -153,5 +156,6 @@ signing_key:
|
|
153
156
|
specification_version: 3
|
154
157
|
summary: see README
|
155
158
|
test_files:
|
159
|
+
- spec/either_spec.rb
|
156
160
|
- spec/option_spec.rb
|
157
161
|
- spec/spec_helper.rb
|