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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 168526a86344e4c8cbab6c7a61cc5d498db3d404
4
- data.tar.gz: cdfa647cd679132878d0deab9cbae65a1c8a9ecd
3
+ metadata.gz: 8a1e76d13413c85adaf4807bc4967275b85a0436
4
+ data.tar.gz: c3eae53b159a16996c9f384c15b5274fe90b14f1
5
5
  SHA512:
6
- metadata.gz: e4280111607d96258055bef389cc7f2c046ed3fe09f3762467b55e3b14ed63257fe43453aef46a28615bfd19b65c882bffe160cb4cd97c3d613f67ae028cd2df
7
- data.tar.gz: ca9701d45bf4682a57b33ac9b323aba4381db78ba52b3b67c8e838dc7b001056a177a3f33c843adc265019f9439fbc8e82a216f904a51ccc241fd69219df272a
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, it's specialty is flow control of actions that either succeed or fail.
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 :>= :try
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) >> validate >> build_request ** log >> send ** log >> build_response
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
- success { |v| "success #{v}"}
207
- failure { |v| "failure #{v}"}
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
- Values for patterns are good, too:
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
- success ->(v) { v == 1 } {|v| "Success #{v}" }
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
- success(Array) { |v| v.first }
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
- failure(1) { "you'll never get me" }
236
+ Failure(f) { "you'll never get me" }
246
237
  end # => NoMatchError
247
238
  ```
248
239
 
249
- A way to have a catch-all would be using an `any`:
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
- Some(1).value_to_a # => Some([1])
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
- some(1) { |n| n + 1 }
322
- some { 1 }
323
- none { 0 }
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
@@ -6,6 +6,7 @@ module Deterministic; end
6
6
 
7
7
  require 'deterministic/monad'
8
8
  require 'deterministic/match'
9
+ require 'deterministic/enum'
9
10
  require 'deterministic/result'
10
11
  require 'deterministic/option'
11
12
  require 'deterministic/either'
@@ -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