dry-monads 0.4.0 → 1.0.0.beta1
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 +5 -5
- data/.travis.yml +7 -4
- data/CHANGELOG.md +141 -1
- data/CONTRIBUTING.md +1 -1
- data/LICENSE +1 -1
- data/README.md +2 -2
- data/dry-monads.gemspec +2 -1
- data/lib/dry/monads.rb +22 -1
- data/lib/dry/monads/curry.rb +19 -0
- data/lib/dry/monads/do.rb +119 -0
- data/lib/dry/monads/either.rb +63 -0
- data/lib/dry/monads/errors.rb +3 -1
- data/lib/dry/monads/lazy.rb +78 -0
- data/lib/dry/monads/list.rb +109 -12
- data/lib/dry/monads/maybe.rb +50 -24
- data/lib/dry/monads/result.rb +93 -49
- data/lib/dry/monads/result/fixed.rb +2 -2
- data/lib/dry/monads/right_biased.rb +18 -27
- data/lib/dry/monads/task.rb +309 -0
- data/lib/dry/monads/transformer.rb +1 -0
- data/lib/dry/monads/traverse.rb +20 -0
- data/lib/dry/monads/try.rb +111 -39
- data/lib/dry/monads/validated.rb +283 -0
- data/lib/dry/monads/version.rb +2 -1
- data/lib/json/add/dry/monads/maybe.rb +1 -1
- metadata +29 -9
data/lib/dry/monads/errors.rb
CHANGED
@@ -1,14 +1,16 @@
|
|
1
1
|
module Dry
|
2
2
|
module Monads
|
3
|
+
# An unsuccessful result of extracting a value from a monad.
|
3
4
|
class UnwrapError < StandardError
|
4
5
|
def initialize(ctx)
|
5
6
|
super("value! was called on #{ ctx.inspect }")
|
6
7
|
end
|
7
8
|
end
|
8
9
|
|
10
|
+
# An error thrown on returning a Failure of unknown type.
|
9
11
|
class InvalidFailureTypeError < StandardError
|
10
12
|
def initialize(failure)
|
11
|
-
super("Cannot create Failure from #{ failure.inspect }, it doesn't meet constraints")
|
13
|
+
super("Cannot create Failure from #{ failure.inspect }, it doesn't meet the constraints")
|
12
14
|
end
|
13
15
|
end
|
14
16
|
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'concurrent/promise'
|
2
|
+
|
3
|
+
require 'dry/monads/task'
|
4
|
+
|
5
|
+
module Dry
|
6
|
+
module Monads
|
7
|
+
# Lazy is a twin of Task which is always executed on the current thread.
|
8
|
+
# The underlying mechanism provided by concurrent-ruby ensures the given
|
9
|
+
# computation is evaluated not more than once (compare with the built-in
|
10
|
+
# lazy assignement ||= which does not guarantee this).
|
11
|
+
class Lazy < Task
|
12
|
+
class << self
|
13
|
+
# @private
|
14
|
+
def new(promise = nil, &block)
|
15
|
+
if promise
|
16
|
+
super(promise)
|
17
|
+
else
|
18
|
+
super(Concurrent::Promise.new(executor: :immediate, &block))
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private :[]
|
23
|
+
end
|
24
|
+
|
25
|
+
# Forces the compution and returns its value.
|
26
|
+
#
|
27
|
+
# @return [Object]
|
28
|
+
def value!
|
29
|
+
@promise.execute.value!
|
30
|
+
end
|
31
|
+
alias_method :force!, :value!
|
32
|
+
|
33
|
+
# Forces the computation. Note that if the computation
|
34
|
+
# thrown an error it won't be re-raised as opposed to value!/force!.
|
35
|
+
#
|
36
|
+
# @return [Lazy]
|
37
|
+
def force
|
38
|
+
@promise.execute
|
39
|
+
self
|
40
|
+
end
|
41
|
+
|
42
|
+
# @return [String]
|
43
|
+
def to_s
|
44
|
+
state = case promise.state
|
45
|
+
when :fulfilled
|
46
|
+
value!.inspect
|
47
|
+
when :rejected
|
48
|
+
"!#{ promise.reason.inspect }"
|
49
|
+
else
|
50
|
+
'?'
|
51
|
+
end
|
52
|
+
|
53
|
+
"Lazy(#{ state })"
|
54
|
+
end
|
55
|
+
alias_method :inspect, :to_s
|
56
|
+
|
57
|
+
# Lazy constructors
|
58
|
+
#
|
59
|
+
module Mixin
|
60
|
+
# @see Dry::Monads::Lazy
|
61
|
+
Lazy = Lazy
|
62
|
+
|
63
|
+
# Lazy constructors
|
64
|
+
module Constructors
|
65
|
+
# Lazy computation contructor
|
66
|
+
#
|
67
|
+
# @param block [Proc]
|
68
|
+
# @return [Lazy]
|
69
|
+
def Lazy(&block)
|
70
|
+
Lazy.new(&block)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
include Constructors
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
data/lib/dry/monads/list.rb
CHANGED
@@ -1,9 +1,17 @@
|
|
1
1
|
require 'dry/equalizer'
|
2
|
+
require 'dry/core/deprecations'
|
3
|
+
|
2
4
|
require 'dry/monads/maybe'
|
5
|
+
require 'dry/monads/task'
|
6
|
+
require 'dry/monads/result'
|
7
|
+
require 'dry/monads/try'
|
8
|
+
require 'dry/monads/validated'
|
3
9
|
require 'dry/monads/transformer'
|
10
|
+
require 'dry/monads/curry'
|
4
11
|
|
5
12
|
module Dry
|
6
13
|
module Monads
|
14
|
+
# The List monad.
|
7
15
|
class List
|
8
16
|
class << self
|
9
17
|
# Builds a list.
|
@@ -23,7 +31,13 @@ module Dry
|
|
23
31
|
if value.nil?
|
24
32
|
List.new([], type)
|
25
33
|
elsif value.respond_to?(:to_ary)
|
26
|
-
|
34
|
+
values = value.to_ary
|
35
|
+
|
36
|
+
if !values.empty? && type.nil? && values[0].respond_to?(:monad)
|
37
|
+
List.new(values, values[0].monad)
|
38
|
+
else
|
39
|
+
List.new(values, type)
|
40
|
+
end
|
27
41
|
else
|
28
42
|
raise TypeError, "Can't coerce #{value.inspect} to List"
|
29
43
|
end
|
@@ -33,11 +47,19 @@ module Dry
|
|
33
47
|
#
|
34
48
|
# @param value [Object] any object
|
35
49
|
# @return [List]
|
36
|
-
def pure(value, type = nil)
|
37
|
-
|
50
|
+
def pure(value = Undefined, type = nil, &block)
|
51
|
+
if value.equal?(Undefined)
|
52
|
+
new([block])
|
53
|
+
elsif block
|
54
|
+
new([block], value)
|
55
|
+
else
|
56
|
+
new([value], type)
|
57
|
+
end
|
38
58
|
end
|
39
59
|
end
|
40
60
|
|
61
|
+
extend Dry::Core::Deprecations[:'dry-monads']
|
62
|
+
|
41
63
|
include Dry::Equalizer(:value, :type)
|
42
64
|
include Transformer
|
43
65
|
|
@@ -200,7 +222,7 @@ module Dry
|
|
200
222
|
#
|
201
223
|
# @return [Maybe<Object>]
|
202
224
|
def head
|
203
|
-
Maybe.coerce(value.first)
|
225
|
+
Monads::Maybe.coerce(value.first)
|
204
226
|
end
|
205
227
|
|
206
228
|
# Returns list's tail.
|
@@ -218,8 +240,13 @@ module Dry
|
|
218
240
|
def typed(type = nil)
|
219
241
|
if type.nil?
|
220
242
|
if size.zero?
|
221
|
-
raise ArgumentError, "Cannot infer monad for an empty list"
|
243
|
+
raise ArgumentError, "Cannot infer a monad for an empty list"
|
222
244
|
else
|
245
|
+
self.class.warn(
|
246
|
+
"Automatic monad inference is deprecated, pass a type explicitly "\
|
247
|
+
"or use a predefined constant, e.g. List::Result\n"\
|
248
|
+
"#{caller.find { |l| l !~ %r{(lib/dry/monads)|(gems)} }}"
|
249
|
+
)
|
223
250
|
self.class.new(value, value[0].monad)
|
224
251
|
end
|
225
252
|
else
|
@@ -244,17 +271,28 @@ module Dry
|
|
244
271
|
# List<Maybe>[Some(1), None, Some(3)].traverse # => None
|
245
272
|
#
|
246
273
|
# @return [Monad] Result is a monadic value
|
247
|
-
def traverse
|
274
|
+
def traverse(proc = nil, &block)
|
248
275
|
unless typed?
|
249
276
|
raise StandardError, "Cannot traverse an untyped list"
|
250
277
|
end
|
251
278
|
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
279
|
+
cons = type.pure { |list, i| list + List.pure(i) }
|
280
|
+
with = proc || block || Traverse[type]
|
281
|
+
|
282
|
+
foldl(type.pure(EMPTY)) do |acc, el|
|
283
|
+
cons.
|
284
|
+
apply(acc).
|
285
|
+
apply { with.(el) }
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
# Applies the stored functions to the elements of the given list.
|
290
|
+
#
|
291
|
+
# @param list [List]
|
292
|
+
# @return [List]
|
293
|
+
def apply(list = Undefined)
|
294
|
+
v = Undefined.default(list) { yield }
|
295
|
+
fmap(Curry).bind { |f| v.fmap { |x| f.(x) } }
|
258
296
|
end
|
259
297
|
|
260
298
|
# Returns the List monad.
|
@@ -264,6 +302,13 @@ module Dry
|
|
264
302
|
List
|
265
303
|
end
|
266
304
|
|
305
|
+
# Returns self.
|
306
|
+
#
|
307
|
+
# @return [Result::Success, Result::Failure]
|
308
|
+
def to_monad
|
309
|
+
self
|
310
|
+
end
|
311
|
+
|
267
312
|
private
|
268
313
|
|
269
314
|
def coerce(other)
|
@@ -273,10 +318,60 @@ module Dry
|
|
273
318
|
# Empty list
|
274
319
|
EMPTY = List.new([].freeze).freeze
|
275
320
|
|
321
|
+
# @private
|
322
|
+
class ListBuilder
|
323
|
+
class << self
|
324
|
+
alias_method :[], :new
|
325
|
+
end
|
326
|
+
|
327
|
+
attr_reader :type
|
328
|
+
|
329
|
+
def initialize(type)
|
330
|
+
@type = type
|
331
|
+
end
|
332
|
+
|
333
|
+
def [](*args)
|
334
|
+
List.new(args, type)
|
335
|
+
end
|
336
|
+
|
337
|
+
def coerce(value)
|
338
|
+
List.coerce(value, type)
|
339
|
+
end
|
340
|
+
|
341
|
+
def pure(val = Undefined, &block)
|
342
|
+
value = Undefined.default(val, block)
|
343
|
+
List.pure(value, type)
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
# List of tasks
|
348
|
+
Task = ListBuilder[Task]
|
349
|
+
|
350
|
+
# List of results
|
351
|
+
Result = ListBuilder[Result]
|
352
|
+
|
353
|
+
# List of results
|
354
|
+
Maybe = ListBuilder[Maybe]
|
355
|
+
|
356
|
+
# List of results
|
357
|
+
Try = ListBuilder[Try]
|
358
|
+
|
359
|
+
# List of validation results
|
360
|
+
Validated = ListBuilder[Validated]
|
361
|
+
|
362
|
+
# List contructors.
|
363
|
+
#
|
364
|
+
# @api public
|
276
365
|
module Mixin
|
366
|
+
|
367
|
+
# @see Dry::Monads::List
|
277
368
|
List = List
|
369
|
+
|
370
|
+
# @see Dry::Monads::List
|
278
371
|
L = List
|
279
372
|
|
373
|
+
# List constructor.
|
374
|
+
# @return [List]
|
280
375
|
def List(value)
|
281
376
|
List.coerce(value)
|
282
377
|
end
|
@@ -284,3 +379,5 @@ module Dry
|
|
284
379
|
end
|
285
380
|
end
|
286
381
|
end
|
382
|
+
|
383
|
+
require 'dry/monads/traverse'
|
data/lib/dry/monads/maybe.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
require 'dry/equalizer'
|
2
|
-
require 'dry/core/deprecations'
|
3
2
|
require 'dry/core/constants'
|
3
|
+
require 'dry/core/deprecations'
|
4
4
|
|
5
5
|
require 'dry/monads/right_biased'
|
6
6
|
require 'dry/monads/transformer'
|
@@ -13,12 +13,12 @@ module Dry
|
|
13
13
|
class Maybe
|
14
14
|
include Transformer
|
15
15
|
|
16
|
-
extend Dry::Core::Deprecations[:'dry-monads']
|
17
|
-
|
18
16
|
class << self
|
17
|
+
extend Dry::Core::Deprecations[:'dry-monads']
|
18
|
+
|
19
19
|
# Wraps the given value with into a Maybe object.
|
20
20
|
#
|
21
|
-
# @param value [Object]
|
21
|
+
# @param value [Object] value to be stored in the monad
|
22
22
|
# @return [Maybe::Some, Maybe::None]
|
23
23
|
def coerce(value)
|
24
24
|
if value.nil?
|
@@ -27,14 +27,15 @@ module Dry
|
|
27
27
|
Some.new(value)
|
28
28
|
end
|
29
29
|
end
|
30
|
-
|
30
|
+
deprecate :lift, :coerce
|
31
31
|
|
32
32
|
# Wraps the given value with `Some`.
|
33
33
|
#
|
34
|
-
# @param value [Object]
|
34
|
+
# @param value [Object] value to be wrapped with Some
|
35
|
+
# @param block [Object] block to be wrapped with Some
|
35
36
|
# @return [Maybe::Some]
|
36
|
-
def pure(value)
|
37
|
-
Some.new(value)
|
37
|
+
def pure(value = Undefined, &block)
|
38
|
+
Some.new(Undefined.default(value, block))
|
38
39
|
end
|
39
40
|
end
|
40
41
|
|
@@ -57,6 +58,13 @@ module Dry
|
|
57
58
|
self
|
58
59
|
end
|
59
60
|
|
61
|
+
# Returns self.
|
62
|
+
#
|
63
|
+
# @return [Maybe::Some, Maybe::None]
|
64
|
+
def to_monad
|
65
|
+
self
|
66
|
+
end
|
67
|
+
|
60
68
|
# Returns the Maybe monad.
|
61
69
|
# This is how we're doing polymorphism in Ruby 😕
|
62
70
|
#
|
@@ -85,10 +93,10 @@ module Dry
|
|
85
93
|
# Dry::Monads.Some(4).fmap(&:succ).fmap(->(n) { n**2 }) # => Some(25)
|
86
94
|
#
|
87
95
|
# @param args [Array<Object>] arguments will be transparently passed through to #bind
|
88
|
-
# @return [Maybe::Some, Maybe::None]
|
96
|
+
# @return [Maybe::Some, Maybe::None] Wrapped result, i.e. nil will be mapped to None,
|
89
97
|
# other values will be wrapped with Some
|
90
98
|
def fmap(*args, &block)
|
91
|
-
self.class.
|
99
|
+
self.class.coerce(bind(*args, &block))
|
92
100
|
end
|
93
101
|
|
94
102
|
# @return [String]
|
@@ -107,6 +115,15 @@ module Dry
|
|
107
115
|
@instance = new.freeze
|
108
116
|
singleton_class.send(:attr_reader, :instance)
|
109
117
|
|
118
|
+
# Line where the value was constructed
|
119
|
+
#
|
120
|
+
# @return [String]
|
121
|
+
attr_reader :trace
|
122
|
+
|
123
|
+
def initialize(trace = RightBiased::Left.trace_caller)
|
124
|
+
@trace = trace
|
125
|
+
end
|
126
|
+
|
110
127
|
# If a block is given passes internal value to it and returns the result,
|
111
128
|
# otherwise simply returns the parameter val.
|
112
129
|
#
|
@@ -125,7 +142,7 @@ module Dry
|
|
125
142
|
end
|
126
143
|
end
|
127
144
|
|
128
|
-
# A lifted version of `#or`. Applies `Maybe.
|
145
|
+
# A lifted version of `#or`. Applies `Maybe.coerce` to the passed value or
|
129
146
|
# to the block result.
|
130
147
|
#
|
131
148
|
# @example
|
@@ -136,7 +153,7 @@ module Dry
|
|
136
153
|
# @return [Maybe::Some, Maybe::None] Lifted `#or` result, i.e. nil will be mapped to None,
|
137
154
|
# other values will be wrapped with Some
|
138
155
|
def or_fmap(*args, &block)
|
139
|
-
Maybe.
|
156
|
+
Maybe.coerce(self.or(*args, &block))
|
140
157
|
end
|
141
158
|
|
142
159
|
# @return [String]
|
@@ -151,7 +168,7 @@ module Dry
|
|
151
168
|
end
|
152
169
|
alias_method :==, :eql?
|
153
170
|
|
154
|
-
# @
|
171
|
+
# @private
|
155
172
|
def hash
|
156
173
|
None.instance.object_id
|
157
174
|
end
|
@@ -159,31 +176,40 @@ module Dry
|
|
159
176
|
|
160
177
|
# A module that can be included for easier access to Maybe monads.
|
161
178
|
module Mixin
|
179
|
+
# @see Dry::Monads::Maybe
|
162
180
|
Maybe = Maybe
|
181
|
+
# @see Maybe::Some
|
163
182
|
Some = Some
|
183
|
+
# @see Maybe::None
|
164
184
|
None = None
|
165
185
|
|
186
|
+
# @private
|
166
187
|
module Constructors
|
167
188
|
# @param value [Object] the value to be stored in the monad
|
168
189
|
# @return [Maybe::Some, Maybe::None]
|
169
190
|
def Maybe(value)
|
170
|
-
Maybe.
|
191
|
+
Maybe.coerce(value)
|
171
192
|
end
|
172
193
|
|
173
|
-
#
|
174
|
-
#
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
194
|
+
# Some constructor
|
195
|
+
#
|
196
|
+
# @overload Some(value)
|
197
|
+
# @param value [Object] any value except `nil`
|
198
|
+
# @return [Maybe::Some]
|
199
|
+
#
|
200
|
+
# @overload Some(&block)
|
201
|
+
# @param block [Proc] a block to be wrapped with Some
|
202
|
+
# @return [Maybe::Some]
|
203
|
+
#
|
204
|
+
def Some(value = Undefined, &block)
|
205
|
+
v = Undefined.default(value, block)
|
206
|
+
raise ArgumentError, 'No value given' if !value.nil? && v.nil?
|
207
|
+
Some.new(v)
|
182
208
|
end
|
183
209
|
|
184
210
|
# @return [Maybe::None]
|
185
211
|
def None
|
186
|
-
None.
|
212
|
+
None.new(RightBiased::Left.trace_caller)
|
187
213
|
end
|
188
214
|
end
|
189
215
|
|