deterministic 0.14.1 → 0.15.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Gem Version](https://badge.fury.io/rb/deterministic.png)](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
|