monadic 0.1.1 → 0.2.0
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/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
|