monadic 0.0.7 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +14 -0
- data/README.md +3 -1
- data/examples/validation.rb +31 -0
- data/lib/monadic/either.rb +33 -23
- data/lib/monadic/maybe.rb +4 -6
- data/lib/monadic/monad.rb +7 -0
- data/lib/monadic/validation.rb +9 -4
- data/lib/monadic/version.rb +1 -1
- data/spec/either_spec.rb +16 -1
- data/spec/maybe_spec.rb +1 -1
- data/spec/monad_spec.rb +10 -0
- data/spec/validation_spec.rb +1 -1
- metadata +3 -2
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,19 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## v0.1.0
|
4
|
+
|
5
|
+
You can now chain Eithers with `+` without providing a block or a proc:
|
6
|
+
|
7
|
+
Success(1) + Failure(2) == Failure(2)
|
8
|
+
Failure(1) + Failure(2) == Failure(1)
|
9
|
+
|
10
|
+
All monads now have the `#to_ary` and `#to_a` method, which coerces the inner value into an `Array`.
|
11
|
+
|
12
|
+
I am considering the Api now almost stable, the only thing I am not so sure about,
|
13
|
+
is whether to apply the magic coercion or not.
|
14
|
+
|
15
|
+
`Validation()` now returns the successfully validated values. See examples/validation.rb
|
16
|
+
|
3
17
|
## v0.0.7
|
4
18
|
|
5
19
|
Implements the #map method for all Monads. It works on value types and on Enumerable collections.
|
data/README.md
CHANGED
@@ -216,7 +216,9 @@ Example:
|
|
216
216
|
end
|
217
217
|
end
|
218
218
|
|
219
|
-
The above example, returns either `Success()` or `Failure(['Age must be > 0', 'No drunks allowed']) with a list of what went wrong during the validation.
|
219
|
+
The above example, returns either `Success([32, :sober, :male])` or `Failure(['Age must be > 0', 'No drunks allowed'])` with a list of what went wrong during the validation.
|
220
|
+
|
221
|
+
See also exmaples/validation.rb
|
220
222
|
|
221
223
|
### Monad
|
222
224
|
All Monads inherit from this class. Standalone it is an Identity monad. Not useful on its own. It's methods are usable on all its descendants.
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'monadic'
|
2
|
+
|
3
|
+
valid_age = ->(age_expr) {
|
4
|
+
age = age_expr.to_i
|
5
|
+
case
|
6
|
+
when age <= 0; Failure('Age must be > 0')
|
7
|
+
when age > 130; Failure('Age must be < 130')
|
8
|
+
else Success(age)
|
9
|
+
end
|
10
|
+
}
|
11
|
+
|
12
|
+
valid_name = ->(name) {
|
13
|
+
case
|
14
|
+
when name =~ /\w{3,99}/i; Success(name)
|
15
|
+
else Failure('Invalid name')
|
16
|
+
end
|
17
|
+
}
|
18
|
+
|
19
|
+
params = {age: 32, name: 'Andrzej', postcode: '50000'}
|
20
|
+
|
21
|
+
Person = Struct.new(:name, :age)
|
22
|
+
|
23
|
+
result = Validation() do
|
24
|
+
check { valid_age.(params[:age]) }
|
25
|
+
check { valid_name.(params[:name]) }
|
26
|
+
end
|
27
|
+
|
28
|
+
case result
|
29
|
+
when Failure; puts "Something went wrong: #{result.fetch}"
|
30
|
+
when Success; person = Person.new(*result.fetch); puts "We have a person #{person.inspect}"
|
31
|
+
end
|
data/lib/monadic/either.rb
CHANGED
@@ -6,6 +6,7 @@ module Monadic
|
|
6
6
|
end
|
7
7
|
|
8
8
|
def self.unit(value)
|
9
|
+
return value if value.is_a? Either
|
9
10
|
return Failure.new(value) if value.nil? || (value.respond_to?(:empty?) && value.empty?) || !value
|
10
11
|
return Success.new(value)
|
11
12
|
end
|
@@ -14,37 +15,46 @@ module Monadic
|
|
14
15
|
# are required (Either is abstract).
|
15
16
|
private_class_method :new
|
16
17
|
|
17
|
-
|
18
|
-
|
19
|
-
|
18
|
+
# Allows privileged access to the +Either+'s inner value from within a block.
|
19
|
+
# This block should return a +Success+ or +Failure+ itself. It will be coerced into #Either
|
20
|
+
# @return [Success, Failure]
|
21
|
+
def bind(proc=nil, &block)
|
22
|
+
return self if failure?
|
23
|
+
return concat(proc) if proc.is_a? Either
|
20
24
|
|
21
|
-
|
22
|
-
|
25
|
+
begin
|
26
|
+
Either(call(proc, block))
|
27
|
+
rescue Exception => ex
|
28
|
+
Failure(ex)
|
29
|
+
end
|
23
30
|
end
|
31
|
+
alias :>= :bind
|
32
|
+
alias :+ :bind
|
24
33
|
|
25
34
|
def fetch(default=@value)
|
26
35
|
return default if failure?
|
27
36
|
return @value
|
28
37
|
end
|
38
|
+
|
29
39
|
alias :_ :fetch
|
40
|
+
def success?
|
41
|
+
is_a? Success
|
42
|
+
end
|
30
43
|
|
31
|
-
def
|
32
|
-
|
44
|
+
def failure?
|
45
|
+
is_a? Failure
|
46
|
+
end
|
33
47
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
Failure(ex)
|
44
|
-
end
|
48
|
+
private
|
49
|
+
def call(proc=nil, block)
|
50
|
+
func = (proc || block)
|
51
|
+
raise "No block or lambda given" unless func.is_a? Proc
|
52
|
+
(func.arity == 0 ? func.call : func.call(@value)) || Failure(nil)
|
53
|
+
end
|
54
|
+
|
55
|
+
def concat(other)
|
56
|
+
failure? ? self : other
|
45
57
|
end
|
46
|
-
alias :>= :bind
|
47
|
-
alias :+ :bind
|
48
58
|
end
|
49
59
|
|
50
60
|
class Either::Chain
|
@@ -64,7 +74,7 @@ module Monadic
|
|
64
74
|
end
|
65
75
|
end
|
66
76
|
|
67
|
-
# @private instance and class methods for Success and Failure
|
77
|
+
# @private instance and class methods for +Success+ and +Failure+
|
68
78
|
module SuccessFailure
|
69
79
|
def self.included(base)
|
70
80
|
base.class_eval do
|
@@ -103,8 +113,8 @@ module Monadic
|
|
103
113
|
Success.new(value)
|
104
114
|
end
|
105
115
|
|
106
|
-
#
|
107
|
-
# @return [Success, Failure] depending whether +value+ is falsey
|
116
|
+
# Coerces any value into either a +Success+ or a +Failure+.
|
117
|
+
# @return [Success, Failure] depending whether +value+ is falsey i.e. nil, false, Enumerable#empty? become +Failure+, all other `Success`
|
108
118
|
def Either(value)
|
109
119
|
Either.unit(value)
|
110
120
|
end
|
data/lib/monadic/maybe.rb
CHANGED
@@ -9,16 +9,12 @@ module Monadic
|
|
9
9
|
# are required (Maybe is abstract).
|
10
10
|
private_class_method :new
|
11
11
|
|
12
|
+
# @return true if the underlying object responds to #empty?, false otherwise
|
12
13
|
def empty?
|
13
14
|
@value.respond_to?(:empty?) && @value.empty?
|
14
15
|
end
|
15
16
|
|
16
|
-
|
17
|
-
return [@value].flatten if @value.respond_to? :flatten
|
18
|
-
return [@value]
|
19
|
-
end
|
20
|
-
alias :to_a :to_ary
|
21
|
-
|
17
|
+
# @return [Failure, Success] the Maybe Monad filtered with the block or proc expression
|
22
18
|
def select(proc = nil, &block)
|
23
19
|
return Maybe(@value.select(&block)) if @value.is_a?(::Enumerable)
|
24
20
|
return Nothing unless (proc || block).call(@value)
|
@@ -30,6 +26,7 @@ module Monadic
|
|
30
26
|
end
|
31
27
|
end
|
32
28
|
|
29
|
+
# Represents an existing value
|
33
30
|
class Just < Maybe
|
34
31
|
public_class_method :new
|
35
32
|
|
@@ -43,6 +40,7 @@ module Monadic
|
|
43
40
|
end
|
44
41
|
end
|
45
42
|
|
43
|
+
# Represents a NullObject
|
46
44
|
class Nothing < Maybe
|
47
45
|
class << self
|
48
46
|
def fetch(default=nil)
|
data/lib/monadic/monad.rb
CHANGED
@@ -8,6 +8,7 @@ module Monadic
|
|
8
8
|
@value = join(value)
|
9
9
|
end
|
10
10
|
|
11
|
+
# Allows priviledged access to the inner value of the Monad from within the block
|
11
12
|
def bind(proc=nil, &block)
|
12
13
|
(proc || block).call(@value)
|
13
14
|
end
|
@@ -35,6 +36,12 @@ module Monadic
|
|
35
36
|
return self.class.unit(func.call(@value))
|
36
37
|
end
|
37
38
|
|
39
|
+
# @return [Array] a with the values inside the monad
|
40
|
+
def to_ary
|
41
|
+
Array(@value)
|
42
|
+
end
|
43
|
+
alias :to_a :to_ary
|
44
|
+
|
38
45
|
# Return the string representation of the Monad
|
39
46
|
def to_s
|
40
47
|
pretty_class_name = self.class.name.split('::')[-1]
|
data/lib/monadic/validation.rb
CHANGED
@@ -9,20 +9,25 @@ module Monadic
|
|
9
9
|
# Validation is not a monad, but an deemed an applicative functor
|
10
10
|
class Validation
|
11
11
|
def initialize
|
12
|
-
@result =
|
12
|
+
@result = []
|
13
13
|
end
|
14
14
|
|
15
15
|
def call(&block)
|
16
16
|
instance_eval(&block)
|
17
|
+
|
18
|
+
if @result.any?(&:failure?)
|
19
|
+
Failure(@result.select(&:failure?).map(&:fetch))
|
20
|
+
else
|
21
|
+
Success(@result.select(&:success?).map(&:fetch))
|
22
|
+
end
|
17
23
|
end
|
18
24
|
|
19
25
|
#
|
20
26
|
def check(proc=nil, &block)
|
21
27
|
result = (proc || block).call
|
22
28
|
raise NotEitherError, "Expected #{result.inspect} to be an Either" unless result.is_a? Either
|
23
|
-
|
24
|
-
|
25
|
-
@result
|
29
|
+
# @failure.fetch << result.fetch) if result.failure?
|
30
|
+
@result << result
|
26
31
|
end
|
27
32
|
end
|
28
33
|
end
|
data/lib/monadic/version.rb
CHANGED
data/spec/either_spec.rb
CHANGED
@@ -75,6 +75,12 @@ describe Monadic::Either do
|
|
75
75
|
Failure.should === Either(nil)
|
76
76
|
end
|
77
77
|
|
78
|
+
it 'Either on an either returns the Either self to allow coercion' do
|
79
|
+
Either(Success(1)).should == Success(1)
|
80
|
+
Success(Success(1)).should == Success(1)
|
81
|
+
Failure(Failure(1)).should == Failure(1)
|
82
|
+
end
|
83
|
+
|
78
84
|
it 'Either with a non-falsey value returns success' do
|
79
85
|
Success.should === Either(1)
|
80
86
|
Success.should === Either(true)
|
@@ -166,6 +172,15 @@ describe Monadic::Either do
|
|
166
172
|
either.should == Success(1)
|
167
173
|
end
|
168
174
|
|
175
|
+
it 'allows to add Eithers without specifying a block or proc' do
|
176
|
+
(Failure(1) + Failure(2)).should == Failure(1)
|
177
|
+
(Success(1) + Failure(2)).should == Failure(2)
|
178
|
+
(Success(1) + Success(2)).should == Success(2)
|
179
|
+
(Failure(1) >= Failure(2)).should == Failure(1)
|
180
|
+
(Success(1) >= Failure(2)).should == Failure(2)
|
181
|
+
(Success(1) >= Success(2)).should == Success(2)
|
182
|
+
end
|
183
|
+
|
169
184
|
it 'allows to check whether the result was a success or failure' do
|
170
185
|
Success(0).success?.should be_true
|
171
186
|
Success(1).failure?.should be_false
|
@@ -174,7 +189,7 @@ describe Monadic::Either do
|
|
174
189
|
Failure(3).failure?.should be_true
|
175
190
|
end
|
176
191
|
|
177
|
-
it '
|
192
|
+
it 'supports Either.chain' do
|
178
193
|
Either.chain do
|
179
194
|
bind -> { Success(1) }
|
180
195
|
bind -> { Success(2) }
|
data/spec/maybe_spec.rb
CHANGED
@@ -115,7 +115,7 @@ describe Monadic::Maybe do
|
|
115
115
|
Maybe([1, 2]).select { |e| e == 1 }.should == Just([1])
|
116
116
|
end
|
117
117
|
|
118
|
-
it 'acts as an array' do
|
118
|
+
it 'acts as an array (inherited from Monad)' do
|
119
119
|
Maybe('foo').to_a.should == ['foo']
|
120
120
|
Maybe(['foo', 'bar']).to_a.should == ['foo', 'bar']
|
121
121
|
Maybe(nil).to_a.should == []
|
data/spec/monad_spec.rb
CHANGED
@@ -8,6 +8,10 @@ describe Monadic::Monad do
|
|
8
8
|
it '#to_s shows the monad name and its value' do
|
9
9
|
Monad.unit(1).to_s.should == 'Monad(1)'
|
10
10
|
Monad.unit(nil).to_s.should == 'Monad(nil)'
|
11
|
+
Monad.unit([1, 2]).map(&:to_s).should == Monad.unit(["1", "2"])
|
12
|
+
|
13
|
+
# can be done also
|
14
|
+
Monad.unit([1, 2]).bind {|v| Monad.unit(v.map(&:to_s)) }.should == Monad.unit(["1", "2"])
|
11
15
|
end
|
12
16
|
|
13
17
|
describe '#map' do
|
@@ -21,4 +25,10 @@ describe Monadic::Monad do
|
|
21
25
|
Monad.unit(['foo', 'bar']).map(&:upcase).should == Monad.unit(['FOO', 'BAR'])
|
22
26
|
end
|
23
27
|
end
|
28
|
+
|
29
|
+
describe '#to_ary #to_a' do
|
30
|
+
Monad.unit([1, 2]).to_a.should == [1, 2]
|
31
|
+
Monad.unit(nil).to_a.should == []
|
32
|
+
Monad.unit('foo').to_a.should == ['foo']
|
33
|
+
end
|
24
34
|
end
|
data/spec/validation_spec.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.1.0
|
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-
|
12
|
+
date: 2012-05-06 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rspec
|
@@ -139,6 +139,7 @@ files:
|
|
139
139
|
- LICENSE
|
140
140
|
- README.md
|
141
141
|
- Rakefile
|
142
|
+
- examples/validation.rb
|
142
143
|
- lib/monadic.rb
|
143
144
|
- lib/monadic/core_ext/object.rb
|
144
145
|
- lib/monadic/either.rb
|