deterministic 0.14.1 → 0.15.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.
- checksums.yaml +4 -4
- data/README.md +108 -41
- data/lib/deterministic.rb +1 -0
- data/lib/deterministic/enum.rb +203 -0
- data/lib/deterministic/monad.rb +19 -5
- data/lib/deterministic/option.rb +40 -80
- data/lib/deterministic/protocol.rb +103 -0
- data/lib/deterministic/result.rb +49 -91
- data/lib/deterministic/version.rb +1 -1
- data/spec/examples/amount_spec.rb +67 -0
- data/spec/examples/config_spec.rb +5 -2
- data/spec/examples/controller_spec.rb +1 -1
- data/spec/examples/list.rb +183 -0
- data/spec/examples/list_spec.rb +186 -0
- data/spec/examples/logger_spec.rb +13 -8
- data/spec/examples/validate_address_spec.rb +5 -4
- data/spec/lib/deterministic/class_mixin_spec.rb +3 -4
- data/spec/lib/deterministic/core_ext/result_spec.rb +4 -2
- data/spec/lib/deterministic/currify_spec.rb +88 -0
- data/spec/lib/deterministic/monad_spec.rb +3 -3
- data/spec/lib/deterministic/option_spec.rb +107 -98
- data/spec/lib/deterministic/protocol_spec.rb +43 -0
- data/spec/lib/deterministic/result/failure_spec.rb +1 -7
- data/spec/lib/deterministic/result/{result_map.rb → result_map_spec.rb} +9 -9
- data/spec/lib/deterministic/result/success_spec.rb +1 -2
- data/spec/lib/deterministic/result_spec.rb +44 -15
- data/spec/lib/enum_spec.rb +108 -0
- data/spec/spec_helper.rb +2 -1
- metadata +17 -8
- data/.rspec +0 -2
- data/spec/examples/bookings_spec.rb +0 -72
- data/spec/lib/deterministic/result/match_spec.rb +0 -116
data/lib/deterministic/monad.rb
CHANGED
@@ -3,15 +3,15 @@ module Deterministic
|
|
3
3
|
class NotMonadError < StandardError; end
|
4
4
|
|
5
5
|
# Basicly the `pure` function
|
6
|
-
def initialize(
|
7
|
-
@value = join(
|
6
|
+
def initialize(init)
|
7
|
+
@value = join(init)
|
8
8
|
end
|
9
9
|
|
10
10
|
# If the passed value is monad already, get the value to avoid nesting
|
11
11
|
# M[M[A]] is equivalent to M[A]
|
12
|
-
def join(
|
13
|
-
if
|
14
|
-
else
|
12
|
+
def join(other)
|
13
|
+
if other.is_a? self.class then other.value
|
14
|
+
else other end
|
15
15
|
end
|
16
16
|
|
17
17
|
# The functor: takes a function (a -> b) and applies it to the inner value of the monad (Ma),
|
@@ -27,6 +27,20 @@ module Deterministic
|
|
27
27
|
# the self.class, i.e. the containing monad is passed as a second (optional) arg to the function
|
28
28
|
def bind(proc=nil, &block)
|
29
29
|
(proc || block).call(value).tap do |result|
|
30
|
+
|
31
|
+
# def parent_name(obj)
|
32
|
+
# parts = obj.class.name.split('::')
|
33
|
+
# if parts.count > 1
|
34
|
+
# parts[0..-2].join('::')
|
35
|
+
# else
|
36
|
+
# parts[0]
|
37
|
+
# end
|
38
|
+
# end
|
39
|
+
|
40
|
+
# self_parent = parent_name(self)
|
41
|
+
# other_parent = parent_name(result)
|
42
|
+
# raise NotMonadError, "Expected #{result.inspect} to be an #{other_parent}" unless self_parent == other_parent
|
43
|
+
|
30
44
|
parent = self.class.superclass === Object ? self.class : self.class.superclass
|
31
45
|
raise NotMonadError, "Expected #{result.inspect} to be an #{parent}" unless result.is_a? parent
|
32
46
|
end
|
data/lib/deterministic/option.rb
CHANGED
@@ -1,28 +1,11 @@
|
|
1
1
|
module Deterministic
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
module PatternMatching
|
7
|
-
include Deterministic::PatternMatching
|
8
|
-
class Match
|
9
|
-
include Deterministic::PatternMatching::Match
|
10
|
-
|
11
|
-
%w[Some None Option].each do |s|
|
12
|
-
define_method s.downcase.to_sym do |value=nil, &block|
|
13
|
-
klas = self.class.module_eval(s)
|
14
|
-
push(klas, value, block)
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
include PatternMatching
|
2
|
+
Option = Deterministic::enum {
|
3
|
+
Some(:s)
|
4
|
+
None()
|
5
|
+
}
|
21
6
|
|
22
|
-
|
7
|
+
class Option
|
23
8
|
class << self
|
24
|
-
protected :new
|
25
|
-
|
26
9
|
def some?(expr)
|
27
10
|
to_option(expr) { expr.nil? }
|
28
11
|
end
|
@@ -39,82 +22,59 @@ module Deterministic
|
|
39
22
|
yield rescue None.new
|
40
23
|
end
|
41
24
|
end
|
25
|
+
end
|
42
26
|
|
43
|
-
|
44
|
-
|
45
|
-
bind(proc || block)
|
46
|
-
end
|
27
|
+
impl(Option) {
|
28
|
+
class NoneValueError < StandardError; end
|
47
29
|
|
48
|
-
|
49
|
-
|
50
|
-
|
30
|
+
def fmap(&fn)
|
31
|
+
match {
|
32
|
+
Some(s) { |m| m.class.new(fn.(s)) }
|
33
|
+
None() { |n| n }
|
34
|
+
}
|
51
35
|
end
|
52
36
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
other.match {
|
58
|
-
some { v + other.value }
|
59
|
-
none { self }
|
60
|
-
}
|
37
|
+
def map(&fn)
|
38
|
+
match {
|
39
|
+
Some(s) { |m| m.bind(&fn) }
|
40
|
+
None() { |n| n }
|
61
41
|
}
|
62
42
|
end
|
63
43
|
|
64
44
|
def some?
|
65
|
-
is_a? Some
|
45
|
+
is_a? Option::Some
|
66
46
|
end
|
67
47
|
|
68
48
|
def none?
|
69
|
-
is_a? None
|
49
|
+
is_a? Option::None
|
70
50
|
end
|
71
51
|
|
72
|
-
def value_or(
|
73
|
-
|
74
|
-
|
52
|
+
def value_or(n)
|
53
|
+
match {
|
54
|
+
Some(s) { s }
|
55
|
+
None() { n }
|
56
|
+
}
|
75
57
|
end
|
76
58
|
|
77
|
-
|
78
|
-
|
59
|
+
def value_to_a
|
60
|
+
@value
|
79
61
|
end
|
80
62
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
end
|
89
|
-
|
90
|
-
def initialize(*args)
|
91
|
-
@value = self
|
92
|
-
end
|
93
|
-
|
94
|
-
def inspect
|
95
|
-
"None"
|
96
|
-
end
|
97
|
-
|
98
|
-
private :value
|
99
|
-
|
100
|
-
def fmap(*args)
|
101
|
-
self
|
102
|
-
end
|
103
|
-
|
104
|
-
alias :map :fmap
|
105
|
-
|
106
|
-
def ==(other)
|
107
|
-
other.class == self.class
|
108
|
-
end
|
63
|
+
def +(other)
|
64
|
+
match {
|
65
|
+
None() { other }
|
66
|
+
Some(_, where { !other.is_a?(Option)}) { raise TypeError, "Other must be an #{Option}"}
|
67
|
+
Some(s, where { other.some? }) { Option::Some.new(s + other.value) }
|
68
|
+
Some(_) { |s| s }
|
69
|
+
}
|
109
70
|
end
|
110
|
-
|
71
|
+
}
|
111
72
|
|
112
|
-
|
113
|
-
|
114
|
-
|
73
|
+
module Prelude
|
74
|
+
module Option
|
75
|
+
None = Deterministic::Option::None.new
|
76
|
+
def Some(s); Deterministic::Option::Some.new(s); end
|
77
|
+
def None(); Deterministic::Prelude::Option::None; end
|
78
|
+
end
|
115
79
|
end
|
116
|
-
|
117
|
-
Some = Deterministic::Option::Some
|
118
|
-
None = Deterministic::Option::None.instance
|
119
80
|
end
|
120
|
-
# p Deterministic::Option::Some::Match.new(.methods
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module Deterministic
|
2
|
+
class ProtocolBuilder
|
3
|
+
|
4
|
+
def initialize(typevar, block)
|
5
|
+
@typevar, @block = typevar, block
|
6
|
+
@protocol = Class.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def build
|
10
|
+
instance_exec(&@block)
|
11
|
+
@protocol
|
12
|
+
end
|
13
|
+
|
14
|
+
def method_missing(m, *args)
|
15
|
+
[m, args]
|
16
|
+
end
|
17
|
+
|
18
|
+
Signature = Struct.new(:name, :params, :return_type, :block)
|
19
|
+
|
20
|
+
def fn(signature, &block)
|
21
|
+
m = signature.to_a.flatten
|
22
|
+
name = m[0]
|
23
|
+
return_type = m[-1]
|
24
|
+
params = Hash[(m[1..-2][0] || {}).map { |k, v| [k[0], v] }]
|
25
|
+
|
26
|
+
@protocol.instance_eval {
|
27
|
+
define_singleton_method(name) {
|
28
|
+
Signature.new(name, params, return_type, block)
|
29
|
+
}
|
30
|
+
}
|
31
|
+
|
32
|
+
@protocol.instance_eval {
|
33
|
+
if block
|
34
|
+
define_method(name) { |*args|
|
35
|
+
block.call(args)
|
36
|
+
}
|
37
|
+
end
|
38
|
+
}
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class InstanceBuilder
|
43
|
+
def initialize(protocol, type, block)
|
44
|
+
@protocol, @type, @block = protocol, type, block
|
45
|
+
@instance = Class.new(@protocol::Protocol)
|
46
|
+
end
|
47
|
+
|
48
|
+
def build
|
49
|
+
@instance.class_exec(&@block)
|
50
|
+
protocol = @protocol::Protocol
|
51
|
+
methods = protocol.methods(false)
|
52
|
+
inst_type = @type
|
53
|
+
|
54
|
+
@instance.instance_exec {
|
55
|
+
methods.each { |name|
|
56
|
+
if method_defined?(name)
|
57
|
+
meth = instance_method(name)
|
58
|
+
signature = protocol.send(name)
|
59
|
+
params = signature.params
|
60
|
+
expect_type = inst_type[signature.return_type]
|
61
|
+
|
62
|
+
define_method(name) { |*args|
|
63
|
+
args.each_with_index { |arg, i|
|
64
|
+
name = params.keys[i]
|
65
|
+
arg_type = params.fetch(name)
|
66
|
+
expect_arg_type = inst_type.fetch(arg_type)
|
67
|
+
|
68
|
+
raise TypeError, "Expected arg #{name} to be a #{expect_arg_type}, got #<#{arg.class}: #{arg.inspect}>" unless arg.is_a? expect_arg_type
|
69
|
+
}
|
70
|
+
|
71
|
+
result = meth.bind(self).call(*args)
|
72
|
+
raise TypeError, "Expected #{name}(#{args.join(', ')}) to return a #{expect_type}, got #<#{result.class}: #{result.inspect}>" unless result.is_a? expect_type
|
73
|
+
result
|
74
|
+
}
|
75
|
+
end
|
76
|
+
}
|
77
|
+
}
|
78
|
+
|
79
|
+
missing = methods.detect { |m| !@instance.instance_methods(false).include?(m) }
|
80
|
+
|
81
|
+
raise NotImplementedError, "`#{missing}` has no default implementation for #{@protocol} #{@type.to_s}" unless missing.nil?
|
82
|
+
|
83
|
+
@instance
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
module_function
|
88
|
+
def protocol(typevar, &block)
|
89
|
+
protocol = ProtocolBuilder.new(typevar, block).build
|
90
|
+
p_module = block.binding.eval('self')
|
91
|
+
p_module.const_set(:Protocol, protocol)
|
92
|
+
end
|
93
|
+
|
94
|
+
def instance(protocol, type, &block)
|
95
|
+
InstanceBuilder.new(protocol, type, block).build
|
96
|
+
end
|
97
|
+
|
98
|
+
module Protocol
|
99
|
+
def const_missing(c)
|
100
|
+
c
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
data/lib/deterministic/result.rb
CHANGED
@@ -1,122 +1,80 @@
|
|
1
|
-
module Deterministic
|
2
|
-
# Abstract parent of Success and Failure
|
3
|
-
class Result
|
4
|
-
include Monad
|
5
1
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
2
|
+
module Deterministic
|
3
|
+
Result = Deterministic::enum {
|
4
|
+
Success(:s)
|
5
|
+
Failure(:f)
|
6
|
+
}
|
10
7
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
end
|
8
|
+
Deterministic::impl(Result) {
|
9
|
+
def map(proc=nil, &block)
|
10
|
+
match {
|
11
|
+
Success(_) { |s| s.bind(proc || block) }
|
12
|
+
Failure(_) { |f| f }
|
13
|
+
}
|
18
14
|
end
|
19
15
|
|
20
|
-
|
21
|
-
|
22
|
-
# This is an abstract class, can't ever instantiate it directly
|
23
|
-
class << self
|
24
|
-
protected :new
|
25
|
-
end
|
16
|
+
alias :>> :map
|
17
|
+
alias :and_then :map
|
26
18
|
|
27
|
-
def
|
28
|
-
|
19
|
+
def map_err(proc=nil, &block)
|
20
|
+
match {
|
21
|
+
Success(_) { |s| s }
|
22
|
+
Failure(_) { |f| f.bind(proc|| block) }
|
23
|
+
}
|
29
24
|
end
|
30
25
|
|
31
|
-
|
32
|
-
is_a? Failure
|
33
|
-
end
|
26
|
+
alias :or_else :map_err
|
34
27
|
|
35
|
-
# `pipe(self: Result(a), op: |Result(a)| -> b) -> Result(a)`
|
36
|
-
# Executes the block passed, but completely ignores its result. If an error is raised within the block it will **NOT** be catched.
|
37
28
|
def pipe(proc=nil, &block)
|
38
29
|
(proc || block).call(self)
|
39
30
|
self
|
40
31
|
end
|
41
32
|
|
42
|
-
alias
|
43
|
-
alias :<< :pipe
|
33
|
+
alias :<< :pipe
|
44
34
|
|
45
|
-
|
46
|
-
|
47
|
-
def and(other)
|
48
|
-
return self if failure?
|
49
|
-
raise NotMonadError, "Expected #{other.inspect} to be an Result" unless other.is_a? Result
|
50
|
-
other
|
51
|
-
end
|
52
|
-
|
53
|
-
# `or(self: Failure(a), other: Result(b)) -> Result(b)`
|
54
|
-
# Replaces `Failure a` with `Result`. If a `Failure` is passed as argument, it is ignored.
|
55
|
-
def or(other)
|
56
|
-
return self if success?
|
57
|
-
raise NotMonadError, "Expected #{other.inspect} to be an Result" unless other.is_a? Result
|
58
|
-
return other
|
35
|
+
def success?
|
36
|
+
is_a? Result::Success
|
59
37
|
end
|
60
38
|
|
61
|
-
|
62
|
-
|
63
|
-
def map(proc=nil, &block)
|
64
|
-
return self if failure?
|
65
|
-
bind(proc || block)
|
39
|
+
def failure?
|
40
|
+
is_a? Result::Failure
|
66
41
|
end
|
67
42
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
return self if success?
|
75
|
-
bind(proc || block)
|
43
|
+
def or(other)
|
44
|
+
raise Deterministic::Monad::NotMonadError, "Expected #{other.inspect} to be a Result" unless other.is_a? Result
|
45
|
+
match {
|
46
|
+
Success(_) { |s| s }
|
47
|
+
Failure(_) { other}
|
48
|
+
}
|
76
49
|
end
|
77
50
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
rescue => err
|
85
|
-
Result::Failure.new(err)
|
51
|
+
def and(other)
|
52
|
+
raise Deterministic::Monad::NotMonadError, "Expected #{other.inspect} to be a Result" unless other.is_a? Result
|
53
|
+
match {
|
54
|
+
Success(_) { other }
|
55
|
+
Failure(_) { |f| f }
|
56
|
+
}
|
86
57
|
end
|
87
58
|
|
88
|
-
# `+(self: Result(a), other: Result(a)) -> Result(a -> a)`
|
89
|
-
# Add the inner values of two Result
|
90
59
|
def +(other)
|
91
|
-
raise Deterministic::Monad::NotMonadError, "Expected #{other.inspect} to be
|
60
|
+
raise Deterministic::Monad::NotMonadError, "Expected #{other.inspect} to be a Result" unless other.is_a? Result
|
92
61
|
match {
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
62
|
+
Success(s, where { other.success?} ) { Result::Success.new(s + other.value) }
|
63
|
+
Failure(f, where { other.failure?} ) { Result::Failure.new(f + other.value) }
|
64
|
+
Success(_) { other } # implied other.failure?
|
65
|
+
Failure(_) { |f| f } # implied other.success?
|
97
66
|
}
|
98
67
|
end
|
68
|
+
}
|
69
|
+
end
|
99
70
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
class Success < Result
|
107
|
-
class << self; public :new; end
|
71
|
+
module Deterministic
|
72
|
+
module Prelude
|
73
|
+
module Result
|
74
|
+
def Success(s); Deterministic::Result::Success.new(s); end
|
75
|
+
def Failure(f); Deterministic::Result::Failure.new(f); end
|
108
76
|
end
|
109
|
-
end
|
110
77
|
|
111
|
-
|
112
|
-
def Success(value)
|
113
|
-
Result::Success.new(value)
|
78
|
+
include Result
|
114
79
|
end
|
115
|
-
|
116
|
-
def Failure(value)
|
117
|
-
Result::Failure.new(value)
|
118
|
-
end
|
119
|
-
|
120
|
-
Success = Result::Success
|
121
|
-
Failure = Result::Failure
|
122
80
|
end
|