monadic 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +11 -0
- data/README.md +28 -8
- data/lib/monadic/either.rb +7 -0
- data/lib/monadic/maybe.rb +3 -2
- data/lib/monadic/monad.rb +7 -3
- data/lib/monadic/version.rb +1 -1
- data/spec/either_spec.rb +8 -0
- data/spec/maybe_spec.rb +1 -1
- data/spec/monad_axioms.rb +1 -20
- data/spec/monad_spec.rb +11 -11
- metadata +2 -2
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,16 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## v0.2.0 (not released)
|
4
|
+
**BREAKING CHANGES**
|
5
|
+
|
6
|
+
`Monad#map` does not recognize `Enumerables` automatically anymore. It just maps, as a `Functor`.
|
7
|
+
Instead the new `Monad#flat_map` function operates on the underlying value as on `Enumerable`.
|
8
|
+
|
9
|
+
`Either#else` allows to exchange the inner value of `Nothing` to an alternative value.
|
10
|
+
|
11
|
+
Either(false == true).else('false was not true') == Failure(false was not true)
|
12
|
+
Success('truth needs no sugar coating').else('all lies') == Success('truth needs no sugar coating')
|
13
|
+
|
3
14
|
## v0.1.1
|
4
15
|
|
5
16
|
`Either()` coerces only `StandardError` to `Failure`, other exceptions higher in the hierarchy are will break the flow.
|
data/README.md
CHANGED
@@ -64,12 +64,17 @@ Map, select:
|
|
64
64
|
```ruby
|
65
65
|
Maybe(123).map { |value| User.find(value) } == Just(someUser) # if user found
|
66
66
|
Maybe(0).map { |value| User.find(value) } == Nothing # if user not found
|
67
|
-
Maybe([1,2]).map { |value| value.to_s } == Just(["1
|
67
|
+
Maybe([1,2]).map { |value| value.to_s } == Just(["1, 2"]) # for all Enumerables
|
68
68
|
|
69
69
|
Maybe('foo').select { |value| value.start_with?('f') } == Just('foo')
|
70
70
|
Maybe('bar').select { |value| value.start_with?('f') } == Nothing
|
71
71
|
```
|
72
72
|
|
73
|
+
For `Enumerable` use `#flat_map`:
|
74
|
+
```ruby
|
75
|
+
Maybe([1, 2]).flat_map {|v| v + 1 } == Just([2, 3])
|
76
|
+
```
|
77
|
+
|
73
78
|
Treat it like an array:
|
74
79
|
|
75
80
|
```ruby
|
@@ -137,7 +142,7 @@ What is specific to this implementation is that exceptions are caught within the
|
|
137
142
|
`Success` represents a successfull execution of an operation (Right in Scala, Haskell).
|
138
143
|
`Failure` represents a failure to execute an operation (Left in Scala, Haskell).
|
139
144
|
|
140
|
-
The `Either()` wrapper will treat all falsey values `nil`, `false` or `empty?` as a `Failure` and all others as `Success`. If that does not suit you, use `Success` or `Failure` only.
|
145
|
+
The `Either()` wrapper works like a coercon. It will treat all falsey values `nil`, `false` or `empty?` as a `Failure` and all others as `Success`. If that does not suit you, use `Success` or `Failure` only. However as ruby cannot enforce the value returned from within a bind, it will auto-magically coerce the return value into an `Either`.
|
141
146
|
|
142
147
|
```ruby
|
143
148
|
result = parse_and_validate_params(params). # must return a Success or Failure inside
|
@@ -192,6 +197,13 @@ Success(params).
|
|
192
197
|
bind ->(path) { load_stuff(params) } #
|
193
198
|
```
|
194
199
|
|
200
|
+
`Either#else` allows to provide alternate values in case of `Failure`:
|
201
|
+
|
202
|
+
```ruby
|
203
|
+
Either(false == true).else('false was not true') == Failure(false was not true)
|
204
|
+
Success('truth needs no sugar coating').else('all lies') == Success('truth needs no sugar coating')
|
205
|
+
```
|
206
|
+
|
195
207
|
Storing intermediate results in instance variables is possible, although it is not very elegant:
|
196
208
|
|
197
209
|
```ruby
|
@@ -247,13 +259,21 @@ The above example, returns either `Success([32, :sober, :male])` or `Failure(['A
|
|
247
259
|
See also [examples/validation.rb](https://github.com/pzol/monadic/blob/master/examples/validation.rb) and [examples/validation_module](https://github.com/pzol/monadic/blob/master/examples/validation_module.rb)
|
248
260
|
|
249
261
|
### Monad
|
250
|
-
All Monads
|
262
|
+
All Monads include this module. Standalone it is an Identity monad. Not useful on its own. It's methods are usable on all its descendants.
|
251
263
|
|
252
264
|
__#map__ is used to map the inner value
|
253
265
|
|
254
266
|
```ruby
|
255
|
-
|
256
|
-
|
267
|
+
# minimum implementation of a monad
|
268
|
+
class Identity
|
269
|
+
include Monadic::Monad
|
270
|
+
def self.unit(value)
|
271
|
+
new(value)
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
Identity.unit('FOO').map(&:capitalize).map {|v| "Hello #{v}"} == Identity(Hello Foo)
|
276
|
+
Identity.unit([1,2]).map {|v| v + 1} == Identity([2, 3])
|
257
277
|
```
|
258
278
|
|
259
279
|
__#bind__ allows (priviledged) access to the boxed value. This is the traditional _no-magic_ `#bind` as found in Haskell,
|
@@ -262,16 +282,16 @@ You` are responsible for re-wrapping the value into a Monad again.
|
|
262
282
|
```ruby
|
263
283
|
# due to the way it works, it will simply return the value, don't rely on this though, different Monads may
|
264
284
|
# implement bind differently (e.g. Maybe involves some _magic_)
|
265
|
-
|
285
|
+
Identity.unit('foo').bind(&:capitalize) == Foo
|
266
286
|
|
267
287
|
# proper use
|
268
|
-
|
288
|
+
Identity.unit('foo').bind {|v| Identity.unit(v.capitalize) } == Identity(Foo)
|
269
289
|
```
|
270
290
|
|
271
291
|
__#fetch__ extracts the inner value of the Monad, some Monads will override this standard behaviour, e.g. the Maybe Monad
|
272
292
|
|
273
293
|
```ruby
|
274
|
-
|
294
|
+
Identity.unit('foo').fetch == "foo"
|
275
295
|
```
|
276
296
|
|
277
297
|
## References
|
data/lib/monadic/either.rb
CHANGED
@@ -32,6 +32,13 @@ module Monadic
|
|
32
32
|
alias :>= :bind
|
33
33
|
alias :+ :bind
|
34
34
|
|
35
|
+
# If it is a Failure it will return a new Failure with the provided value
|
36
|
+
# @return [Success, Failure]
|
37
|
+
def else(value)
|
38
|
+
return Failure(value) if failure?
|
39
|
+
return self
|
40
|
+
end
|
41
|
+
|
35
42
|
def fetch(default=@value)
|
36
43
|
return default if failure?
|
37
44
|
return @value
|
data/lib/monadic/maybe.rb
CHANGED
@@ -18,8 +18,9 @@ module Monadic
|
|
18
18
|
|
19
19
|
# @return [Failure, Success] the Maybe Monad filtered with the block or proc expression
|
20
20
|
def select(proc = nil, &block)
|
21
|
-
|
22
|
-
return
|
21
|
+
func = (proc || block)
|
22
|
+
return Maybe(@value.select {|v| func.call(v) }) if @value.respond_to? :select
|
23
|
+
return Nothing unless func.call(@value)
|
23
24
|
return self
|
24
25
|
end
|
25
26
|
|
data/lib/monadic/monad.rb
CHANGED
@@ -23,15 +23,19 @@ module Monadic
|
|
23
23
|
end
|
24
24
|
alias :_ :fetch
|
25
25
|
|
26
|
-
# A
|
27
|
-
# If the underlying `value` is an `Enumerable`, the map is applied on each element of the collection.
|
26
|
+
# A Functor applying the proc or block on the boxed `value` and returning the Monad with the transformed values.
|
28
27
|
# (A -> B) -> M[A] -> M[B]
|
29
28
|
def map(proc = nil, &block)
|
30
29
|
func = (proc || block)
|
31
|
-
return self.class.unit(@value.map {|v| func.call(v) }) if @value.is_a?(::Enumerable)
|
32
30
|
return self.class.unit(func.call(@value))
|
33
31
|
end
|
34
32
|
|
33
|
+
def flat_map(proc = nil, &block)
|
34
|
+
fail "Underlying value does not respond to #map" unless @value.respond_to? :map
|
35
|
+
func = (proc || block)
|
36
|
+
return self.class.unit(@value.map {|v| func.call(v) }) if @value.respond_to? :map
|
37
|
+
end
|
38
|
+
|
35
39
|
# @return [Array] a with the values inside the monad
|
36
40
|
def to_ary
|
37
41
|
Array(@value)
|
data/lib/monadic/version.rb
CHANGED
data/spec/either_spec.rb
CHANGED
@@ -53,6 +53,14 @@ describe Monadic::Either do
|
|
53
53
|
error.message.should == 'error'
|
54
54
|
end
|
55
55
|
|
56
|
+
it '#else returns an alternative value considered Success if it is Nothing' do
|
57
|
+
Failure(false).else(true).should == Failure(true)
|
58
|
+
Either(nil).else(true).should == Failure(true)
|
59
|
+
Success(true).else(false).should == Success(true)
|
60
|
+
Either(true).else(false).should == Success(true)
|
61
|
+
Success(false).else(true).should == Success(false)
|
62
|
+
end
|
63
|
+
|
56
64
|
class User
|
57
65
|
attr :age, :gender, :sobriety
|
58
66
|
def self.find(id)
|
data/spec/maybe_spec.rb
CHANGED
@@ -105,7 +105,7 @@ describe Monadic::Maybe do
|
|
105
105
|
it 'allows to use map' do
|
106
106
|
Maybe(nil).map { |e| Hash.new(:key => e) }.should == Nothing
|
107
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([
|
108
|
+
Maybe([1,2]).map { |e| e.to_s }.should == Just("[1, 2]")
|
109
109
|
end
|
110
110
|
|
111
111
|
it 'allows to use select' do
|
data/spec/monad_axioms.rb
CHANGED
@@ -34,24 +34,5 @@ shared_examples 'a Monad' do
|
|
34
34
|
id1.should == id2
|
35
35
|
end
|
36
36
|
end
|
37
|
-
|
38
|
-
describe '#map functor' do
|
39
|
-
add100 = ->(value) { value + 100 }
|
40
|
-
it 'on value types, returns the transformed value, wrapped in the Monad' do
|
41
|
-
res = monad.unit(1).map {|v| add100.(v) }
|
42
|
-
res.should == monad.unit(101)
|
43
|
-
|
44
|
-
res = monad.unit(1).map {|v| monad.unit(v + 100) }
|
45
|
-
res.should == monad.unit(101)
|
46
|
-
end
|
47
|
-
|
48
|
-
it 'on enumerables, returns the transformed collection, wrapped in the Monad' do
|
49
|
-
res = monad.unit([1,2,3]).map {|v| add100.(v) }
|
50
|
-
res.should == monad.unit([101,102,103])
|
51
|
-
|
52
|
-
# The following does not work... not sure whether it should
|
53
|
-
# res = monad.unit([1,2,3]).map {|v| monad.unit(v + 100) }
|
54
|
-
# res.should == monad.unit([101,102,103])
|
55
|
-
end
|
56
|
-
end
|
57
37
|
end
|
38
|
+
|
data/spec/monad_spec.rb
CHANGED
@@ -15,27 +15,27 @@ describe Monadic::Monad do
|
|
15
15
|
it '#to_s shows the monad name and its value' do
|
16
16
|
Identity.unit(1).to_s.should == 'Identity(1)'
|
17
17
|
Identity.unit(nil).to_s.should == 'Identity(nil)'
|
18
|
-
Identity.unit([1, 2]).map(&:to_s).should == Identity.unit([
|
18
|
+
Identity.unit([1, 2]).map(&:to_s).should == Identity.unit("[1, 2]")
|
19
19
|
|
20
20
|
# can be done also
|
21
21
|
Identity.unit([1, 2]).bind {|v| Identity.unit(v.map(&:to_s)) }.should == Identity.unit(["1", "2"])
|
22
22
|
end
|
23
23
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
end
|
24
|
+
it '#map applies the function to the underlying value directly' do
|
25
|
+
Identity.unit(1).map {|v| v + 2}.should == Identity.unit(3)
|
26
|
+
Identity.unit('foo').map(&:upcase).should == Identity.unit('FOO')
|
27
|
+
end
|
29
28
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
29
|
+
it 'delegates #flat_map to an underlying collection and wraps the resulting collection' do
|
30
|
+
Identity.unit([1,2]).flat_map {|v| v + 1}.should == Identity.unit([2, 3])
|
31
|
+
Identity.unit(['foo', 'bar']).flat_map(&:upcase).should == Identity.unit(['FOO', 'BAR'])
|
32
|
+
expect { Identity.unit(1).flat_map {|v| v + 1 } }.to raise_error(RuntimeError)
|
34
33
|
end
|
35
34
|
|
36
|
-
|
35
|
+
it '#to_ary #to_a' do
|
37
36
|
Identity.unit([1, 2]).to_a.should == [1, 2]
|
38
37
|
Identity.unit(nil).to_a.should == []
|
39
38
|
Identity.unit('foo').to_a.should == ['foo']
|
40
39
|
end
|
40
|
+
|
41
41
|
end
|
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.
|
4
|
+
version: 0.2.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-17 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rspec
|