deterministic 0.1.1 → 0.1.2
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.
- checksums.yaml +4 -4
- data/README.md +17 -3
- data/lib/deterministic.rb +2 -0
- data/lib/deterministic/either.rb +14 -8
- data/lib/deterministic/either/attempt_all.rb +4 -2
- data/lib/deterministic/either/failure.rb +3 -1
- data/lib/deterministic/either/match.rb +12 -14
- data/lib/deterministic/either/success.rb +3 -1
- data/lib/deterministic/monad.rb +10 -7
- data/lib/deterministic/version.rb +1 -1
- data/spec/examples/bookings_spec.rb +74 -0
- data/spec/examples/validate_address_spec.rb +31 -0
- data/spec/lib/deterministic/attempt_all_spec.rb +14 -6
- data/spec/lib/deterministic/either/match_spec.rb +15 -1
- data/spec/lib/deterministic/either/success_spec.rb +0 -4
- data/spec/lib/deterministic/either_spec.rb +8 -0
- data/spec/lib/deterministic/monad_spec.rb +22 -2
- data/spec/spec_helper.rb +1 -0
- metadata +9 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 24278b8af8e996a30226c448b0677f8669e14606
|
4
|
+
data.tar.gz: 6a67ad79ac2e6ff81e0530b547c658f0cfb4633a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0fd77c14af51a1814426d1c33bffccda8b97a58c5266638503dca676e1cbd78ac8a159becabfb3e4fe262016b5a2d0bf1663111f76d0fcbe92e7d105fa296f52
|
7
|
+
data.tar.gz: 6181518d77416ec8cf02712cd738f56b842deaf212a4ac368b8babcc141c01bf718fb7b9b10f2d11d6109cf36a35db0b3306fce8629cdafca940d239d9a2c675
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Deterministic
|
2
2
|
|
3
|
-
This is a spiritual successor of the [Monadic gem](http://github.com/pzol/monadic).
|
3
|
+
This is a spiritual successor of the [Monadic gem](http://github.com/pzol/monadic). The goal of the rewrite is to get away from a bit to forceful aproach I took in Monadic, especially when it comes to coercing monads, but also a more practical but at the same time more strict adherence to monad laws.
|
4
4
|
|
5
5
|
This gem is still __WORK IN PROGRESS__.
|
6
6
|
|
@@ -60,6 +60,12 @@ However, the real fun starts if you use it with your own context. You can use th
|
|
60
60
|
|
61
61
|
### Pattern matching
|
62
62
|
Now that you have some result, you want to control flow by providing patterns.
|
63
|
+
`#match` can match by
|
64
|
+
|
65
|
+
* success, failure, either or any
|
66
|
+
* values
|
67
|
+
* lambdas
|
68
|
+
* classes
|
63
69
|
|
64
70
|
```ruby
|
65
71
|
Success(1).match do
|
@@ -70,9 +76,9 @@ end # => "either 1"
|
|
70
76
|
```
|
71
77
|
Note1: the inner value has been unwrapped!
|
72
78
|
|
73
|
-
Note2: only the
|
79
|
+
Note2: only the __first__ matching pattern block will be executed, so order __can__ be important.
|
74
80
|
|
75
|
-
The result returned will be the result of the
|
81
|
+
The result returned will be the result of the __first__ `#try` or `#let`. As a side note, `#try` is a monad, `#let` is a functor.
|
76
82
|
|
77
83
|
Values for patterns are good, too:
|
78
84
|
|
@@ -90,6 +96,14 @@ Success(1).match do
|
|
90
96
|
end # => "Success 1"
|
91
97
|
```
|
92
98
|
|
99
|
+
Also you can match the result class
|
100
|
+
|
101
|
+
```ruby
|
102
|
+
Success([1, 2, 3]).match do
|
103
|
+
success(Array) { |v| v.first }
|
104
|
+
end # => 1
|
105
|
+
```
|
106
|
+
|
93
107
|
Combining `#attempt_all` and `#match` is the ultimate sophistication:
|
94
108
|
|
95
109
|
```ruby
|
data/lib/deterministic.rb
CHANGED
data/lib/deterministic/either.rb
CHANGED
@@ -1,12 +1,4 @@
|
|
1
1
|
module Deterministic
|
2
|
-
def Success(value)
|
3
|
-
Success.new(value)
|
4
|
-
end
|
5
|
-
|
6
|
-
def Failure(value)
|
7
|
-
Failure.new(value)
|
8
|
-
end
|
9
|
-
|
10
2
|
class Either
|
11
3
|
include Monad
|
12
4
|
include Deterministic::PatternMatching
|
@@ -23,5 +15,19 @@ module Deterministic
|
|
23
15
|
return self if failure?
|
24
16
|
return other if other.is_a? Either
|
25
17
|
end
|
18
|
+
|
19
|
+
class << self
|
20
|
+
protected :new
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
module_function
|
25
|
+
|
26
|
+
def Success(value)
|
27
|
+
Success.new(value)
|
28
|
+
end
|
29
|
+
|
30
|
+
def Failure(value)
|
31
|
+
Failure.new(value)
|
26
32
|
end
|
27
33
|
end
|
@@ -19,6 +19,7 @@ class Deterministic::Either
|
|
19
19
|
end
|
20
20
|
end
|
21
21
|
|
22
|
+
# This is a functor
|
22
23
|
def try(&block)
|
23
24
|
try_p = ->(acc) {
|
24
25
|
begin
|
@@ -32,10 +33,11 @@ class Deterministic::Either
|
|
32
33
|
@tries << try_p
|
33
34
|
end
|
34
35
|
|
36
|
+
# Basicly a monad
|
35
37
|
def let(sym=nil, &block)
|
36
38
|
@tries << ->(acc) {
|
37
|
-
@context.instance_exec(acc, &block).tap do |value|
|
38
|
-
raise EitherExpectedError unless value.is_a? Either
|
39
|
+
@context.instance_exec(acc.value, &block).tap do |value|
|
40
|
+
raise EitherExpectedError, "Expected the result to be either Success or Failure" unless value.is_a? Either
|
39
41
|
end
|
40
42
|
}
|
41
43
|
end
|
@@ -1,23 +1,24 @@
|
|
1
1
|
module Deterministic::PatternMatching
|
2
2
|
|
3
|
-
def match(
|
4
|
-
match = Match.new(self)
|
5
|
-
match.instance_eval &
|
3
|
+
def match(context=self, &block)
|
4
|
+
match = Match.new(self, context)
|
5
|
+
match.instance_eval &block
|
6
6
|
match.result
|
7
7
|
end
|
8
8
|
|
9
9
|
class NoMatchError < StandardError; end
|
10
10
|
|
11
11
|
class Match
|
12
|
-
def initialize(container)
|
12
|
+
def initialize(container, context)
|
13
13
|
@container = container
|
14
|
+
@context = context
|
14
15
|
@collection = []
|
15
16
|
end
|
16
17
|
|
17
18
|
def result
|
18
|
-
matcher = @collection.
|
19
|
-
raise NoMatchError if matcher.nil?
|
20
|
-
|
19
|
+
matcher = @collection.detect { |m| m.matches?(@container.value) }
|
20
|
+
raise NoMatchError, "No could be made for #{@container}" if matcher.nil?
|
21
|
+
@context.instance_exec(@container.value, &matcher.block)
|
21
22
|
end
|
22
23
|
|
23
24
|
# TODO: Either specific DSL, will need to move it to Either, later on
|
@@ -38,17 +39,14 @@ module Deterministic::PatternMatching
|
|
38
39
|
def matches?(value)
|
39
40
|
condition.call(value)
|
40
41
|
end
|
41
|
-
|
42
|
-
def result(value)
|
43
|
-
block.call(value)
|
44
|
-
end
|
45
42
|
end
|
46
43
|
|
47
44
|
def push(type, condition, result_block)
|
48
45
|
condition_pred = case
|
49
|
-
when condition.nil?;
|
50
|
-
when condition.is_a?(Proc);
|
51
|
-
|
46
|
+
when condition.nil?; ->(v) { true }
|
47
|
+
when condition.is_a?(Proc); condition
|
48
|
+
when condition.is_a?(Class); ->(v) { condition === @container.value }
|
49
|
+
else ->(v) { condition == @container.value }
|
52
50
|
end
|
53
51
|
|
54
52
|
matcher_pred = compose_predicates(type_pred[type], condition_pred)
|
data/lib/deterministic/monad.rb
CHANGED
@@ -15,26 +15,29 @@ module Deterministic
|
|
15
15
|
|
16
16
|
# The functor: takes a function (a -> b) and applies it to the inner value of the monad (Ma),
|
17
17
|
# boxes it back to the same monad (Mb)
|
18
|
-
# fmap :: (a -> b) ->
|
18
|
+
# fmap :: (a -> b) -> M a -> M b
|
19
19
|
def map(proc=nil, &block)
|
20
20
|
result = (proc || block).call(value)
|
21
21
|
self.class.new(result)
|
22
22
|
end
|
23
23
|
|
24
|
-
# The monad: takes a function which returns a monad, applies
|
25
|
-
# bind ::
|
24
|
+
# The monad: takes a function which returns a monad (of the same type), applies the function
|
25
|
+
# bind :: M a -> (a -> Mb) -> M b
|
26
|
+
# the self.class, i.e. the containing monad is passed as a second (optional) arg to the function
|
26
27
|
def bind(proc=nil, &block)
|
27
|
-
|
28
|
-
|
29
|
-
|
28
|
+
(proc || block).call(value, self.class).tap do |result|
|
29
|
+
raise NotMonadError unless result.is_a? self.class
|
30
|
+
end
|
30
31
|
end
|
32
|
+
alias :'>>=' :bind
|
31
33
|
|
32
34
|
# Get the underlying value, return in Haskell
|
33
|
-
# return ::
|
35
|
+
# return :: M a -> a
|
34
36
|
def value
|
35
37
|
@value
|
36
38
|
end
|
37
39
|
|
40
|
+
# Two monads are equivalent if they are of the same type and when their values are equal
|
38
41
|
def ==(other)
|
39
42
|
return false unless other.is_a? self.class
|
40
43
|
@value == other.instance_variable_get(:@value)
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'deterministic'
|
3
|
+
|
4
|
+
include Deterministic
|
5
|
+
|
6
|
+
module Examples
|
7
|
+
module Bookings
|
8
|
+
|
9
|
+
module Adapters
|
10
|
+
class BookingsRepo
|
11
|
+
def find(params)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class FakeWebUi
|
17
|
+
def method_missing(m, *args)
|
18
|
+
{ m => args }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class Dependencies
|
23
|
+
include Adapters
|
24
|
+
def bookings_repo
|
25
|
+
BookingsRepo.new
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class ShowBookings
|
30
|
+
def initialize(world=FakeWebUi.new, deps=Dependencies.new)
|
31
|
+
@world = world
|
32
|
+
@world.booking_list([])
|
33
|
+
end
|
34
|
+
|
35
|
+
def call(dirty_params)
|
36
|
+
result = Either.attempt_all(self) do
|
37
|
+
try { parse_params(dirty_params) }
|
38
|
+
let { |params| read_bookings(params) }
|
39
|
+
end.match(world) do
|
40
|
+
success(Array) { |bookings| booking_list(bookings) }
|
41
|
+
success { |booking| booking(booking) }
|
42
|
+
failure(:no_bookings) { empty_booking_list }
|
43
|
+
failure(StandardError) { |ex| raise ex }
|
44
|
+
any { |result| raise "Something went terribly wrong `#{result.class}`" }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# private
|
49
|
+
attr_reader :bookings_repo, :world
|
50
|
+
|
51
|
+
def parse_params(dirty_params)
|
52
|
+
dirty_params
|
53
|
+
end
|
54
|
+
|
55
|
+
def read_bookings(params)
|
56
|
+
bookings = [params]
|
57
|
+
case bookings
|
58
|
+
when nil; Failure(:no_bookings)
|
59
|
+
when bookings.count == 1;
|
60
|
+
else Success(bookings)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
describe Examples::Bookings::ShowBookings do
|
68
|
+
subject { described_class.new }
|
69
|
+
it "works" do
|
70
|
+
expect(
|
71
|
+
subject.call({status: 'confirmed'})
|
72
|
+
).to eq({:booking_list=>[[{:status=>"confirmed"}]]})
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'deterministic'
|
3
|
+
|
4
|
+
include Deterministic
|
5
|
+
|
6
|
+
# A Unit of Work for validating an address
|
7
|
+
module ValidateAddress
|
8
|
+
def self.call(candidate)
|
9
|
+
errors = {}
|
10
|
+
errors[:street] = "Street cannot be empty" unless candidate.has_key? :street
|
11
|
+
errors[:city] = "Street cannot be empty" unless candidate.has_key? :city
|
12
|
+
errors[:postal] = "Street cannot be empty" unless candidate.has_key? :postal
|
13
|
+
|
14
|
+
errors.empty? ? Success(candidate) : Failure(errors)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe ValidateAddress do
|
19
|
+
subject { ValidateAddress.call(candidate) }
|
20
|
+
context 'sunny day' do
|
21
|
+
let(:candidate) { {title: "Hobbiton", street: "501 Buckland Rd", city: "Matamata", postal: "3472", country: "nz"} }
|
22
|
+
specify { expect(subject).to be_a Success }
|
23
|
+
specify { expect(subject.value).to eq candidate }
|
24
|
+
end
|
25
|
+
|
26
|
+
context 'empty data' do
|
27
|
+
let(:candidate) { {} }
|
28
|
+
specify { expect(subject).to be_a Failure }
|
29
|
+
specify { expect(subject.value).to include(:street, :city, :postal) }
|
30
|
+
end
|
31
|
+
end
|
@@ -1,5 +1,4 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
-
require 'deterministic'
|
3
2
|
|
4
3
|
include Deterministic
|
5
4
|
|
@@ -64,6 +63,15 @@ describe Deterministic::Either::AttemptAll do
|
|
64
63
|
}.to raise_error
|
65
64
|
end
|
66
65
|
|
66
|
+
it "#let passes params unboxed" do
|
67
|
+
expect(
|
68
|
+
Either.attempt_all do
|
69
|
+
try { 1 }
|
70
|
+
let { |v| Success(v + 1) }
|
71
|
+
end
|
72
|
+
).to eq Success(2)
|
73
|
+
end
|
74
|
+
|
67
75
|
it "works with an OpenStruct" do
|
68
76
|
context = OpenStruct.new
|
69
77
|
attempt = Either.attempt_all(context) do
|
@@ -75,23 +83,23 @@ describe Deterministic::Either::AttemptAll do
|
|
75
83
|
expect(context.alpha).to eq 2
|
76
84
|
end
|
77
85
|
|
78
|
-
it "can operate in the context of a
|
79
|
-
class
|
86
|
+
it "can operate in the context of a context" do
|
87
|
+
class Context
|
80
88
|
attr_accessor :a
|
81
89
|
def initialize
|
82
90
|
@a = 1
|
83
91
|
end
|
84
92
|
end
|
85
93
|
|
86
|
-
|
94
|
+
context = Context.new
|
87
95
|
|
88
96
|
expect(
|
89
|
-
Either.attempt_all(
|
97
|
+
Either.attempt_all(context) do
|
90
98
|
try { self.a += 1 }
|
91
99
|
try { self.a + 1 }
|
92
100
|
end
|
93
101
|
).to eq Success(3)
|
94
102
|
|
95
|
-
expect(
|
103
|
+
expect(context.a).to eq 2
|
96
104
|
end
|
97
105
|
end
|
@@ -1,5 +1,4 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
-
require 'deterministic'
|
3
2
|
|
4
3
|
include Deterministic
|
5
4
|
|
@@ -34,6 +33,20 @@ describe Deterministic::Either::Match do
|
|
34
33
|
).to eq "matched 2"
|
35
34
|
end
|
36
35
|
|
36
|
+
it "can match with classes" do
|
37
|
+
expect(
|
38
|
+
Success([1, 2, 3]).match do
|
39
|
+
success(Array) { |v| v.first }
|
40
|
+
end
|
41
|
+
).to eq 1
|
42
|
+
|
43
|
+
expect(
|
44
|
+
Success(1).match do
|
45
|
+
success(Fixnum) { |v| v }
|
46
|
+
end
|
47
|
+
).to eq 1
|
48
|
+
end
|
49
|
+
|
37
50
|
it "catch-all" do
|
38
51
|
expect(
|
39
52
|
Success(1).match do
|
@@ -55,6 +68,7 @@ describe Deterministic::Either::Match do
|
|
55
68
|
it "can match with lambdas" do
|
56
69
|
expect(
|
57
70
|
Success(1).match do
|
71
|
+
failure { "not me" }
|
58
72
|
success ->(v) { v == 1 } { |v| "matched #{v}" }
|
59
73
|
end
|
60
74
|
).to eq "matched 1"
|
@@ -1,6 +1,5 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
require_relative '../monad_axioms'
|
3
|
-
require 'deterministic'
|
4
3
|
|
5
4
|
include Deterministic
|
6
5
|
|
@@ -17,7 +16,6 @@ describe Deterministic::Success do
|
|
17
16
|
specify { expect(subject).to be_success }
|
18
17
|
specify { expect(subject).not_to be_failure }
|
19
18
|
|
20
|
-
# public constructor #Success[]
|
21
19
|
specify { expect(subject).to be_an_instance_of described_class }
|
22
20
|
specify { expect(subject).to eq(described_class.new(1)) }
|
23
21
|
specify { expect(subject << Success(2)).to eq(Success(2)) }
|
@@ -30,6 +28,4 @@ describe Deterministic::Success do
|
|
30
28
|
subject.bind { |v| true ? Success(v + 1) : Failure(v + 2)}
|
31
29
|
).to eq Success(2)
|
32
30
|
end
|
33
|
-
|
34
|
-
specify { expect { Success("a").bind(&:upcase) }.to raise_error(Deterministic::Monad::NotMonadError) }
|
35
31
|
end
|
@@ -1,6 +1,5 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
require_relative 'monad_axioms'
|
3
|
-
require 'deterministic'
|
4
3
|
|
5
4
|
|
6
5
|
describe Deterministic::Monad do
|
@@ -17,7 +16,28 @@ describe Deterministic::Monad do
|
|
17
16
|
specify { expect(Identity.new([1, 2]).map(&:to_s)).to eq Identity.new("[1, 2]") }
|
18
17
|
specify { expect(Identity.new(1).map {|v| v + 2}).to eq Identity.new(3) }
|
19
18
|
specify { expect(Identity.new('foo').map(&:upcase)).to eq Identity.new('FOO')}
|
20
|
-
|
19
|
+
|
20
|
+
context '#bind' do
|
21
|
+
it "raises an error if the passed function does not return a monad of the same class" do
|
22
|
+
expect { Identity.new(1).bind {} }.to raise_error(Deterministic::Monad::NotMonadError)
|
23
|
+
end
|
24
|
+
specify { expect(Identity.new(1).bind {|value| Identity.new(value) }).to eq Identity.new(1) }
|
25
|
+
|
26
|
+
it "passes the monad class, this is ruby-fu?!" do
|
27
|
+
Identity.new(1)
|
28
|
+
.bind do |_, monad|
|
29
|
+
p self.class
|
30
|
+
expect(monad).to eq Identity
|
31
|
+
monad.new(_)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
specify { expect(
|
36
|
+
Identity.new(1).bind { |value, monad| monad.new(value + 1) }
|
37
|
+
).to eq Identity.new(2)
|
38
|
+
}
|
39
|
+
|
40
|
+
end
|
21
41
|
specify { expect(Identity.new(Identity.new(1))).to eq Identity.new(1) }
|
22
42
|
|
23
43
|
# it 'delegates #flat_map to an underlying collection and wraps the resulting collection' do
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: deterministic
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Piotr Zolnierek
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2014-01-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -132,9 +132,12 @@ files:
|
|
132
132
|
- lib/deterministic/either/success.rb
|
133
133
|
- lib/deterministic/monad.rb
|
134
134
|
- lib/deterministic/version.rb
|
135
|
+
- spec/examples/bookings_spec.rb
|
136
|
+
- spec/examples/validate_address_spec.rb
|
135
137
|
- spec/lib/deterministic/attempt_all_spec.rb
|
136
138
|
- spec/lib/deterministic/either/match_spec.rb
|
137
139
|
- spec/lib/deterministic/either/success_spec.rb
|
140
|
+
- spec/lib/deterministic/either_spec.rb
|
138
141
|
- spec/lib/deterministic/monad_axioms.rb
|
139
142
|
- spec/lib/deterministic/monad_spec.rb
|
140
143
|
- spec/spec_helper.rb
|
@@ -158,14 +161,17 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
158
161
|
version: '0'
|
159
162
|
requirements: []
|
160
163
|
rubyforge_project:
|
161
|
-
rubygems_version: 2.0.
|
164
|
+
rubygems_version: 2.0.3
|
162
165
|
signing_key:
|
163
166
|
specification_version: 4
|
164
167
|
summary: see above
|
165
168
|
test_files:
|
169
|
+
- spec/examples/bookings_spec.rb
|
170
|
+
- spec/examples/validate_address_spec.rb
|
166
171
|
- spec/lib/deterministic/attempt_all_spec.rb
|
167
172
|
- spec/lib/deterministic/either/match_spec.rb
|
168
173
|
- spec/lib/deterministic/either/success_spec.rb
|
174
|
+
- spec/lib/deterministic/either_spec.rb
|
169
175
|
- spec/lib/deterministic/monad_axioms.rb
|
170
176
|
- spec/lib/deterministic/monad_spec.rb
|
171
177
|
- spec/spec_helper.rb
|