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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8a1e76d13413c85adaf4807bc4967275b85a0436
|
4
|
+
data.tar.gz: c3eae53b159a16996c9f384c15b5274fe90b14f1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3955dda1c3e00c7a3814d5bb766453a3615c29a65cdec56e21a91bcdf4feff8d55f5f7bb1999d78116306d3bca59d7d30de620bc80f70e6df25b1c6eb7f349f8
|
7
|
+
data.tar.gz: 84da7a63dab19f2fa754ad4694c7b3b00eccf3e5684d00caa5e5d9d854cb4f46075ea597f089685967e6f710cdd1dbe66e36833824d333bd981d452206c6ce75
|
data/README.md
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
[](http://badge.fury.io/rb/deterministic)
|
4
4
|
|
5
|
-
Deterministic is to help your code to be more confident,
|
5
|
+
Deterministic is to help your code to be more confident, by utilizing functional programming patterns.
|
6
6
|
|
7
7
|
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.
|
8
8
|
|
@@ -30,6 +30,9 @@ Deterministic provides different monads, here is a short guide, when to use whic
|
|
30
30
|
#### Maybe
|
31
31
|
- an object may be nil, you want to avoid endless nil? checks
|
32
32
|
|
33
|
+
#### Enums (Algebraic Data Types)
|
34
|
+
- roll your own pattern
|
35
|
+
|
33
36
|
## Usage
|
34
37
|
|
35
38
|
### Result: Success & Failure
|
@@ -103,12 +106,6 @@ Failure(1).or_else { |n| Success(n)} # => Success(1)
|
|
103
106
|
|
104
107
|
Executes the block passed, but completely ignores its result. If an error is raised within the block it will **NOT** be catched.
|
105
108
|
|
106
|
-
```ruby
|
107
|
-
Success(1).try { |n| log(n.value) } # => Success(1)
|
108
|
-
```
|
109
|
-
|
110
|
-
The value or block result must always be a `Result` i.e. `Success` or `Failure`.
|
111
|
-
|
112
109
|
### Result Chaining
|
113
110
|
|
114
111
|
You can easily chain the execution of several operations. Here we got some nice function composition.
|
@@ -118,14 +115,17 @@ The following aliases are defined
|
|
118
115
|
|
119
116
|
```ruby
|
120
117
|
alias :>> :map
|
121
|
-
alias
|
122
|
-
alias :** :pipe # the operator must be right associative
|
118
|
+
alias :<< :pipe
|
123
119
|
```
|
124
120
|
|
125
121
|
This allows the composition of procs or lambdas and thus allow a clear definiton of a pipeline.
|
126
122
|
|
127
123
|
```ruby
|
128
|
-
Success(params) >>
|
124
|
+
Success(params) >>
|
125
|
+
validate >>
|
126
|
+
build_request << log >>
|
127
|
+
send << log >>
|
128
|
+
build_response
|
129
129
|
```
|
130
130
|
|
131
131
|
#### Complex Example in a Builder Class
|
@@ -203,9 +203,8 @@ Now that you have some result, you want to control flow by providing patterns.
|
|
203
203
|
|
204
204
|
```ruby
|
205
205
|
Success(1).match do
|
206
|
-
|
207
|
-
|
208
|
-
result { |v| "result #{v}"}
|
206
|
+
Success(s) { |v| "success #{s}"}
|
207
|
+
Failure(f) { |v| "failure #{f}"}
|
209
208
|
end # => "success 1"
|
210
209
|
```
|
211
210
|
Note1: the inner value has been unwrapped!
|
@@ -214,19 +213,11 @@ Note2: only the __first__ matching pattern block will be executed, so order __ca
|
|
214
213
|
|
215
214
|
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.
|
216
215
|
|
217
|
-
|
218
|
-
|
219
|
-
```ruby
|
220
|
-
Success(1).match do
|
221
|
-
success(1) {|v| "Success #{v}" }
|
222
|
-
end # => "Success 1"
|
223
|
-
```
|
224
|
-
|
225
|
-
You can and should also use procs for patterns:
|
216
|
+
Guards
|
226
217
|
|
227
218
|
```ruby
|
228
219
|
Success(1).match do
|
229
|
-
|
220
|
+
Success(s, where { s == 1 }) { "Success #{s}" }
|
230
221
|
end # => "Success 1"
|
231
222
|
```
|
232
223
|
|
@@ -234,7 +225,7 @@ Also you can match the result class
|
|
234
225
|
|
235
226
|
```ruby
|
236
227
|
Success([1, 2, 3]).match do
|
237
|
-
|
228
|
+
Success(s, where { s.is_a?(Array)} ) { s.first }
|
238
229
|
end # => 1
|
239
230
|
```
|
240
231
|
|
@@ -242,23 +233,15 @@ If no match was found a `NoMatchError` is raised, so make sure you always cover
|
|
242
233
|
|
243
234
|
```ruby
|
244
235
|
Success(1).match do
|
245
|
-
|
236
|
+
Failure(f) { "you'll never get me" }
|
246
237
|
end # => NoMatchError
|
247
238
|
```
|
248
239
|
|
249
|
-
|
250
|
-
|
251
|
-
```ruby
|
252
|
-
Success(1).match do
|
253
|
-
any { "catch-all" }
|
254
|
-
end # => "catch-all"
|
255
|
-
```
|
240
|
+
Matches must be exhaustive, otherwise an error will be raised, showing the variants which have not been covered.
|
256
241
|
|
257
242
|
## core_ext
|
258
243
|
You can use a core extension, to include Result in your own class or in Object, i.e. in all classes.
|
259
244
|
|
260
|
-
|
261
|
-
|
262
245
|
```ruby
|
263
246
|
require 'deterministic/core_ext/object/result'
|
264
247
|
|
@@ -275,23 +258,35 @@ Some(1).some? # #=> true
|
|
275
258
|
Some(1).none? # #=> false
|
276
259
|
None.some? # #=> false
|
277
260
|
None.none? # #=> true
|
261
|
+
```
|
278
262
|
|
263
|
+
Maps an `Option` with the value `a` to the same `Option` with the value `b`.
|
264
|
+
|
265
|
+
```ruby
|
279
266
|
Some(1).fmap { |n| n + 1 } # => Some(2)
|
280
267
|
None.fmap { |n| n + 1 } # => None
|
268
|
+
```
|
281
269
|
|
270
|
+
Maps a `Result` with the value `a` to another `Result` with the value `b`.
|
271
|
+
|
272
|
+
```ruby
|
282
273
|
Some(1).map { |n| Some(n + 1) } # => Some(2)
|
283
274
|
Some(1).map { |n| None } # => None
|
284
275
|
None.map { |n| Some(n + 1) } # => None
|
276
|
+
```
|
285
277
|
|
278
|
+
Get the inner value or provide a default for a `None`. Calling `#value` on a `None` will raise a `NoMethodError`
|
279
|
+
|
280
|
+
```ruby
|
286
281
|
Some(1).value # => 1
|
287
282
|
Some(1).value_or(2) # => 1
|
288
283
|
None.value # => NoMethodError
|
289
284
|
None.value_or(0) # => 0
|
285
|
+
```
|
290
286
|
|
291
|
-
|
292
|
-
Some([1]).value_to_a # => Some([1])
|
293
|
-
None.value_to_a # => None
|
287
|
+
Add the inner values of option using `+`.
|
294
288
|
|
289
|
+
```ruby
|
295
290
|
Some(1) + Some(1) # => Some(2)
|
296
291
|
Some([1]) + Some(1) # => TypeError: No implicit conversion
|
297
292
|
None + Some(1) # => Some(1)
|
@@ -315,15 +310,87 @@ Option.try! { 1 } # => Some(1)
|
|
315
310
|
Option.try! { raise "error"} # => None
|
316
311
|
```
|
317
312
|
|
318
|
-
### Pattern Matching
|
313
|
+
### Pattern Matching
|
319
314
|
```ruby
|
320
315
|
Some(1).match {
|
321
|
-
|
322
|
-
|
323
|
-
|
316
|
+
Some(s, where { s == 1 }) { s + 1 }
|
317
|
+
Some(s) { 1 }
|
318
|
+
None() { 0 }
|
324
319
|
} # => 2
|
325
320
|
```
|
326
321
|
|
322
|
+
## Enums
|
323
|
+
All the above are implemented using enums, see their definition, for more details.
|
324
|
+
|
325
|
+
Define it, with all variants:
|
326
|
+
|
327
|
+
```ruby
|
328
|
+
Threenum = Deterministic::enum {
|
329
|
+
Nullary()
|
330
|
+
Unary(:a)
|
331
|
+
Binary(:a, :b)
|
332
|
+
}
|
333
|
+
|
334
|
+
Threenum.variants # => [:Nullary, :Unary, :Binary]
|
335
|
+
```
|
336
|
+
|
337
|
+
Initialize
|
338
|
+
|
339
|
+
```ruby
|
340
|
+
n = Threenum.Nullary # => Threenum::Nullary.new()
|
341
|
+
n.value # => Error
|
342
|
+
|
343
|
+
u = Threenum.Unary(1) # => Threenum::Unary.new(1)
|
344
|
+
u.value # => 1
|
345
|
+
|
346
|
+
b = Threenum::Binary(2, 3) # => Threenum::Binary(2, 3)
|
347
|
+
b.value # => { a:2, b: 3 }
|
348
|
+
```
|
349
|
+
|
350
|
+
Pattern matching
|
351
|
+
|
352
|
+
```ruby
|
353
|
+
Threenum::Unary(5).match {
|
354
|
+
Nullary() { 0 }
|
355
|
+
Unary(u) { u }
|
356
|
+
Binary(a, b) { a + b }
|
357
|
+
} # => 5
|
358
|
+
|
359
|
+
# or
|
360
|
+
t = Threenum::Unary(5)
|
361
|
+
Threenum.match(t) {
|
362
|
+
Nullary() { 0 }
|
363
|
+
Unary(u) { u }
|
364
|
+
Binary(a, b) { a + b }
|
365
|
+
} # => 5
|
366
|
+
```
|
367
|
+
|
368
|
+
If you want the whole thing use the arg passed to the block (second case)
|
369
|
+
|
370
|
+
```ruby
|
371
|
+
def drop(n)
|
372
|
+
match {
|
373
|
+
Cons(h, t, where { n > 0 }) { t.drop(n - 1) }
|
374
|
+
Cons(_, _) { |c| c }
|
375
|
+
Nil() { raise EmptyListError}
|
376
|
+
}
|
377
|
+
end
|
378
|
+
```
|
379
|
+
|
380
|
+
See the linked list implementation in the specs for more examples
|
381
|
+
|
382
|
+
With guard clauses
|
383
|
+
|
384
|
+
```ruby
|
385
|
+
Threenum::Unary(5).match {
|
386
|
+
Nullary() { 0 }
|
387
|
+
Unary(u) { u }
|
388
|
+
Binary(a, b, where { a.is_a?(Fixnum) && b.is_a?(Fixnum)}) { a + b }
|
389
|
+
Binary(a, b) { raise "Expected a, b to be numbers" }
|
390
|
+
} # => 5
|
391
|
+
```
|
392
|
+
|
393
|
+
All matches must be exhaustive, i.e. cover all variants
|
327
394
|
|
328
395
|
## Maybe
|
329
396
|
The simplest NullObject wrapper there can be. It adds `#some?` and `#null?` to `Object` though.
|
data/lib/deterministic.rb
CHANGED
@@ -0,0 +1,203 @@
|
|
1
|
+
module Deterministic
|
2
|
+
module Enum
|
3
|
+
class MatchError < StandardError; end
|
4
|
+
end
|
5
|
+
|
6
|
+
class EnumBuilder
|
7
|
+
def initialize(parent)
|
8
|
+
@parent = parent
|
9
|
+
end
|
10
|
+
|
11
|
+
class DataType
|
12
|
+
module AnyEnum
|
13
|
+
include Deterministic::Monad
|
14
|
+
|
15
|
+
def match(&block)
|
16
|
+
parent.match(self, &block)
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_s
|
20
|
+
value.to_s
|
21
|
+
end
|
22
|
+
|
23
|
+
def name
|
24
|
+
self.class.name.split("::")[-1]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
module Nullary
|
29
|
+
def initialize(*args)
|
30
|
+
@value = nil
|
31
|
+
end
|
32
|
+
|
33
|
+
def inspect
|
34
|
+
name
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
module Binary
|
39
|
+
def initialize(*init)
|
40
|
+
raise ArgumentError, "Expected arguments for #{args}, got #{init}" unless (init.count == 1 && init[0].is_a?(Hash)) || init.count == args.count
|
41
|
+
if init.count == 1 && init[0].is_a?(Hash)
|
42
|
+
@value = Hash[args.zip(init[0].values)]
|
43
|
+
else
|
44
|
+
@value = Hash[args.zip(init)]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def inspect
|
49
|
+
params = value.map { |k, v| "#{k}: #{v.inspect}" }
|
50
|
+
"#{name}(#{params.join(', ')})"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.create(parent, name, args)
|
55
|
+
raise ArgumentError, "#{args} may not contain the reserved name :value" if args.include? :value
|
56
|
+
dt = Class.new(parent)
|
57
|
+
|
58
|
+
dt.instance_eval {
|
59
|
+
class << self; public :new; end
|
60
|
+
include AnyEnum
|
61
|
+
define_method(:args) { args }
|
62
|
+
|
63
|
+
define_method(:parent) { parent }
|
64
|
+
private :parent
|
65
|
+
}
|
66
|
+
|
67
|
+
if args.count == 0
|
68
|
+
dt.instance_eval {
|
69
|
+
include Nullary
|
70
|
+
private :value
|
71
|
+
}
|
72
|
+
elsif args.count == 1
|
73
|
+
dt.instance_eval {
|
74
|
+
define_method(args[0].to_sym) { value }
|
75
|
+
}
|
76
|
+
else
|
77
|
+
dt.instance_eval {
|
78
|
+
include Binary
|
79
|
+
|
80
|
+
args.each do |m|
|
81
|
+
define_method(m) do
|
82
|
+
@value[m]
|
83
|
+
end
|
84
|
+
end
|
85
|
+
}
|
86
|
+
end
|
87
|
+
dt
|
88
|
+
end
|
89
|
+
|
90
|
+
class << self
|
91
|
+
public :new;
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def method_missing(m, *args)
|
96
|
+
@parent.const_set(m, DataType.create(@parent, m, args))
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
module_function
|
101
|
+
def enum(&block)
|
102
|
+
mod = Class.new do # the enum to be built
|
103
|
+
class << self; private :new; end
|
104
|
+
|
105
|
+
def self.match(obj, &block)
|
106
|
+
matcher = self::Matcher.new(obj)
|
107
|
+
matcher.instance_eval(&block)
|
108
|
+
|
109
|
+
variants_in_match = matcher.matches.collect {|e| e[1].name.split('::')[-1].to_sym}.uniq.sort
|
110
|
+
variants_not_covered = variants - variants_in_match
|
111
|
+
raise Enum::MatchError, "Match is non-exhaustive, #{variants_not_covered} not covered" unless variants_not_covered.empty?
|
112
|
+
|
113
|
+
type_matches = matcher.matches.select { |r| r[0].is_a?(r[1]) }
|
114
|
+
|
115
|
+
type_matches.each { |match|
|
116
|
+
obj, type, block, args, guard = match
|
117
|
+
|
118
|
+
if args.count == 0
|
119
|
+
return instance_exec(obj, &block)
|
120
|
+
else
|
121
|
+
raise Enum::MatchError, "Pattern (#{args.join(', ')}) must match (#{obj.args.join(', ')})" if args.count != obj.args.count
|
122
|
+
context = exec_context(obj, args)
|
123
|
+
|
124
|
+
if guard
|
125
|
+
if context.instance_exec(obj, &guard)
|
126
|
+
return context.instance_exec(obj, &block)
|
127
|
+
end
|
128
|
+
else
|
129
|
+
return context.instance_exec(obj, &block)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
}
|
133
|
+
|
134
|
+
raise Enum::MatchError, "No match could be made"
|
135
|
+
end
|
136
|
+
|
137
|
+
def self.variants; constants - [:Matcher, :MatchError]; end
|
138
|
+
|
139
|
+
private
|
140
|
+
def self.exec_context(obj, args)
|
141
|
+
if obj.is_a?(Deterministic::EnumBuilder::DataType::Binary)
|
142
|
+
Struct.new(*(args)).new(*(obj.value.values))
|
143
|
+
else
|
144
|
+
Struct.new(*(args)).new(obj.value)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
enum = EnumBuilder.new(mod)
|
149
|
+
enum.instance_eval(&block)
|
150
|
+
|
151
|
+
type_variants = mod.constants
|
152
|
+
|
153
|
+
matcher = Class.new {
|
154
|
+
def initialize(obj)
|
155
|
+
@obj = obj
|
156
|
+
@matches = []
|
157
|
+
@vars = []
|
158
|
+
end
|
159
|
+
|
160
|
+
attr_reader :matches, :vars
|
161
|
+
|
162
|
+
def where(&guard)
|
163
|
+
guard
|
164
|
+
end
|
165
|
+
|
166
|
+
def method_missing(m)
|
167
|
+
m
|
168
|
+
end
|
169
|
+
|
170
|
+
type_variants.each { |m|
|
171
|
+
define_method(m) { |*args, &block|
|
172
|
+
raise ArgumentError, "No block given to `#{m}`" if block.nil?
|
173
|
+
type = Kernel.eval("#{mod.name}::#{m}")
|
174
|
+
|
175
|
+
if args.count > 0 && args[-1].is_a?(Proc)
|
176
|
+
guard = args.delete_at(-1)
|
177
|
+
end
|
178
|
+
|
179
|
+
@matches << [@obj, type, block, args, guard]
|
180
|
+
}
|
181
|
+
}
|
182
|
+
}
|
183
|
+
|
184
|
+
mod.const_set(:Matcher, matcher)
|
185
|
+
|
186
|
+
type_variants.each { |variant|
|
187
|
+
mod.singleton_class.class_exec {
|
188
|
+
define_method(variant) { |*args|
|
189
|
+
const_get(variant).new(*args)
|
190
|
+
}
|
191
|
+
}
|
192
|
+
}
|
193
|
+
mod
|
194
|
+
end
|
195
|
+
|
196
|
+
def impl(enum_type, &block)
|
197
|
+
enum_type.variants.each { |v|
|
198
|
+
name = "#{enum_type.name}::#{v.to_s}"
|
199
|
+
type = Kernel.eval(name)
|
200
|
+
type.class_eval(&block)
|
201
|
+
}
|
202
|
+
end
|
203
|
+
end
|