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 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