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
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'dry/monads/validated'
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Monads
|
5
|
+
to_list = List::Validated.method(:pure)
|
6
|
+
|
7
|
+
id = -> x { x }
|
8
|
+
|
9
|
+
# List of default traverse functions for types.
|
10
|
+
# It is implicitly used by List#traverse for
|
11
|
+
# making common cases easier to handle.
|
12
|
+
Traverse = {
|
13
|
+
Validated => -> el { el.alt_map(to_list) }
|
14
|
+
}
|
15
|
+
|
16
|
+
# By default the identity function is used
|
17
|
+
Traverse.default = id
|
18
|
+
Traverse.freeze
|
19
|
+
end
|
20
|
+
end
|
data/lib/dry/monads/try.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'dry/equalizer'
|
2
|
+
require 'dry/core/deprecations'
|
2
3
|
|
3
4
|
require 'dry/monads/right_biased'
|
4
5
|
require 'dry/monads/result'
|
@@ -11,19 +12,66 @@ module Dry
|
|
11
12
|
#
|
12
13
|
# @api public
|
13
14
|
class Try
|
15
|
+
# @private
|
16
|
+
DEFAULT_EXCEPTIONS = [StandardError].freeze
|
17
|
+
|
18
|
+
# @return [Exception] Caught exception
|
14
19
|
attr_reader :exception
|
15
20
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
21
|
+
class << self
|
22
|
+
extend Dry::Core::Deprecations[:'dry-monads']
|
23
|
+
|
24
|
+
# Invokes a callable and if successful stores the result in the
|
25
|
+
# {Try::Value} type, but if one of the specified exceptions was raised it stores
|
26
|
+
# it in a {Try::Error}.
|
27
|
+
#
|
28
|
+
# @param exceptions [Array<Exception>] list of exceptions to rescue
|
29
|
+
# @param f [#call] callable object
|
30
|
+
# @return [Try::Value, Try::Error]
|
31
|
+
def run(exceptions, f)
|
32
|
+
Value.new(exceptions, f.call)
|
33
|
+
rescue *exceptions => e
|
34
|
+
Error.new(e)
|
35
|
+
end
|
36
|
+
deprecate :lift, :run
|
37
|
+
|
38
|
+
# Wraps a value with Value
|
39
|
+
#
|
40
|
+
# @overload pure(value, exceptions = DEFAULT_EXCEPTIONS)
|
41
|
+
# @param value [Object] value for wrapping
|
42
|
+
# @param exceptions [Array<Exceptions>] list of exceptions to rescue
|
43
|
+
# @return [Try::Value]
|
44
|
+
#
|
45
|
+
# @overload pure(exceptions = DEFAULT_EXCEPTIONS, &block)
|
46
|
+
# @param exceptions [Array<Exceptions>] list of exceptions to rescue
|
47
|
+
# @param block [Proc] value for wrapping
|
48
|
+
# @return [Try::Value]
|
49
|
+
#
|
50
|
+
def pure(value = Undefined, exceptions = DEFAULT_EXCEPTIONS, &block)
|
51
|
+
if value.equal?(Undefined)
|
52
|
+
Value.new(DEFAULT_EXCEPTIONS, block)
|
53
|
+
elsif block.nil?
|
54
|
+
Value.new(exceptions, value)
|
55
|
+
else
|
56
|
+
Value.new(value, block)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Safely runs a block
|
61
|
+
#
|
62
|
+
# @example using Try with [] and a block (Ruby 2.5+)
|
63
|
+
# include Dry::Monads::Try::Mixin
|
64
|
+
#
|
65
|
+
# def safe_db_call
|
66
|
+
# Try[DatabaseError] { db_call }
|
67
|
+
# end
|
68
|
+
#
|
69
|
+
# @param exceptions [Array<Exception>]
|
70
|
+
# @return [Try::Value,Try::Error]
|
71
|
+
def [](*exceptions, &block)
|
72
|
+
raise ArgumentError, 'At least one exception type required' if exceptions.empty?
|
73
|
+
run(exceptions, block)
|
74
|
+
end
|
27
75
|
end
|
28
76
|
|
29
77
|
# Returns true for an instance of a {Try::Value} monad.
|
@@ -38,6 +86,13 @@ module Dry
|
|
38
86
|
end
|
39
87
|
alias_method :failure?, :error?
|
40
88
|
|
89
|
+
# Returns self.
|
90
|
+
#
|
91
|
+
# @return [Maybe::Some, Maybe::None]
|
92
|
+
def to_monad
|
93
|
+
self
|
94
|
+
end
|
95
|
+
|
41
96
|
# Represents a result of a successful execution.
|
42
97
|
#
|
43
98
|
# @api public
|
@@ -45,6 +100,7 @@ module Dry
|
|
45
100
|
include Dry::Equalizer(:value!, :catchable)
|
46
101
|
include RightBiased::Right
|
47
102
|
|
103
|
+
# @private
|
48
104
|
attr_reader :catchable
|
49
105
|
|
50
106
|
# @param exceptions [Array<Exception>] list of exceptions to be rescued
|
@@ -104,9 +160,8 @@ module Dry
|
|
104
160
|
|
105
161
|
# @return [Result::Success]
|
106
162
|
def to_result
|
107
|
-
Dry::Monads::Success(@value)
|
163
|
+
Dry::Monads::Result::Success.new(@value)
|
108
164
|
end
|
109
|
-
alias_method :to_either, :to_result
|
110
165
|
|
111
166
|
# @return [String]
|
112
167
|
def to_s
|
@@ -129,14 +184,13 @@ module Dry
|
|
129
184
|
|
130
185
|
# @return [Maybe::None]
|
131
186
|
def to_maybe
|
132
|
-
|
187
|
+
Maybe::None.new(RightBiased::Left.trace_caller)
|
133
188
|
end
|
134
189
|
|
135
190
|
# @return [Result::Failure]
|
136
191
|
def to_result
|
137
|
-
|
192
|
+
Result::Failure.new(exception, RightBiased::Left.trace_caller)
|
138
193
|
end
|
139
|
-
alias_method :to_either, :to_result
|
140
194
|
|
141
195
|
# @return [String]
|
142
196
|
def to_s
|
@@ -185,37 +239,55 @@ module Dry
|
|
185
239
|
# Foo.new(10, 2).average # => 5
|
186
240
|
# Foo.new(10, 0).average # => nil
|
187
241
|
module Mixin
|
242
|
+
# @see Dry::Monads::Try
|
188
243
|
Try = Try
|
189
244
|
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
245
|
+
module Constructors
|
246
|
+
# A convenience wrapper for {Monads::Try.run}.
|
247
|
+
# If no exceptions are provided it falls back to StandardError.
|
248
|
+
# In general, relying on this behaviour is not recommended as it can lead to unnoticed
|
249
|
+
# bugs and it is always better to explicitly specify a list of exceptions if possible.
|
250
|
+
#
|
251
|
+
# @param exceptions [Array<Exception>]
|
252
|
+
# @return [Try]
|
253
|
+
def Try(*exceptions, &f)
|
254
|
+
catchable = exceptions.empty? ? Try::DEFAULT_EXCEPTIONS : exceptions.flatten
|
255
|
+
Try.run(catchable, f)
|
256
|
+
end
|
201
257
|
end
|
202
258
|
|
259
|
+
include Constructors
|
260
|
+
|
261
|
+
# Value constructor
|
262
|
+
#
|
263
|
+
# @overload Value(value)
|
264
|
+
# @param value [Object]
|
265
|
+
# @return [Try::Value]
|
266
|
+
#
|
267
|
+
# @overload Value(&block)
|
268
|
+
# @param block [Proc] a block to be wrapped with Value
|
269
|
+
# @return [Try::Value]
|
270
|
+
#
|
203
271
|
def Value(value = Undefined, exceptions = DEFAULT_EXCEPTIONS, &block)
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
else
|
208
|
-
Try::Value.new(exceptions, value)
|
209
|
-
end
|
272
|
+
v = Undefined.default(value, block)
|
273
|
+
raise ArgumentError, 'No value given' if !value.nil? && v.nil?
|
274
|
+
Value.new(exceptions, v)
|
210
275
|
end
|
211
276
|
|
277
|
+
# Error constructor
|
278
|
+
#
|
279
|
+
# @overload Error(value)
|
280
|
+
# @param error [Exception]
|
281
|
+
# @return [Try::Error]
|
282
|
+
#
|
283
|
+
# @overload Error(&block)
|
284
|
+
# @param block [Proc] a block to be wrapped with Error
|
285
|
+
# @return [Try::Error]
|
286
|
+
#
|
212
287
|
def Error(error = Undefined, &block)
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
else
|
217
|
-
Try::Error.new(error)
|
218
|
-
end
|
288
|
+
v = Undefined.default(error, block)
|
289
|
+
raise ArgumentError, 'No value given' if v.nil?
|
290
|
+
Try::Error.new(v)
|
219
291
|
end
|
220
292
|
end
|
221
293
|
end
|
@@ -0,0 +1,283 @@
|
|
1
|
+
require 'dry/monads/maybe'
|
2
|
+
require 'dry/monads/result'
|
3
|
+
|
4
|
+
module Dry
|
5
|
+
module Monads
|
6
|
+
# Validated is similar to Result and represents an outcome of a validation.
|
7
|
+
# The difference between Validated and Result is that the former implements
|
8
|
+
# `#apply` in a way that concatenates errors. This means that the error type
|
9
|
+
# has to have `+` implemented (be a semigroup). This plays nice with arrays and lists.
|
10
|
+
# Also, List<Validated>#traverse implicitly uses a block that wraps errors with
|
11
|
+
# a list so that you don't have to do it manually.
|
12
|
+
#
|
13
|
+
# @example using with List
|
14
|
+
# List::Validated[Valid('London'), Invalid(:name_missing), Invalid(:email_missing)]
|
15
|
+
# # => Invalid(List[:name_missing, :email_missing])
|
16
|
+
#
|
17
|
+
# @example with valid results
|
18
|
+
# List::Validated[Valid('London'), Valid('John')]
|
19
|
+
# # => Valid(List['London', 'John'])
|
20
|
+
#
|
21
|
+
class Validated
|
22
|
+
class << self
|
23
|
+
# Wraps a value with `Valid`.
|
24
|
+
#
|
25
|
+
# @overload pure(value)
|
26
|
+
# @param value [Object] value to be wrapped with Valid
|
27
|
+
# @return [Validated::Valid]
|
28
|
+
#
|
29
|
+
# @overload pure(&block)
|
30
|
+
# @param block [Object] block to be wrapped with Valid
|
31
|
+
# @return [Validated::Valid]
|
32
|
+
#
|
33
|
+
def pure(value = Undefined, &block)
|
34
|
+
Valid.new(Undefined.default(value, block))
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns self.
|
39
|
+
#
|
40
|
+
# @return [Validated::Valid, Validated::Invalid]
|
41
|
+
def to_monad
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
45
|
+
# Bind/flat_map is not implemented
|
46
|
+
#
|
47
|
+
def bind(*)
|
48
|
+
# See https://typelevel.org/cats/datatypes/validated.html for details on why
|
49
|
+
raise NotImplementedError, "Validated is not a monad because it would violate the monad laws"
|
50
|
+
end
|
51
|
+
|
52
|
+
# Valid result
|
53
|
+
#
|
54
|
+
class Valid < Validated
|
55
|
+
include Dry::Equalizer(:value!)
|
56
|
+
|
57
|
+
def initialize(value)
|
58
|
+
@value = value
|
59
|
+
end
|
60
|
+
|
61
|
+
# Extracts the value
|
62
|
+
#
|
63
|
+
# @return [Object]
|
64
|
+
def value!
|
65
|
+
@value
|
66
|
+
end
|
67
|
+
|
68
|
+
# Applies another Valid to the stored function
|
69
|
+
#
|
70
|
+
# @overload apply(val)
|
71
|
+
# @example
|
72
|
+
# Validated.pure { |x| x + 1 }.apply(Valid(2)) # => Valid(3)
|
73
|
+
#
|
74
|
+
# @param val [Validated::Valid,Validated::Invalid]
|
75
|
+
# @return [Validated::Valid,Validated::Invalid]
|
76
|
+
#
|
77
|
+
# @overload apply
|
78
|
+
# @example
|
79
|
+
# Validated.pure { |x| x + 1 }.apply { Valid(4) } # => Valid(5)
|
80
|
+
#
|
81
|
+
# @yieldreturn [Validated::Valid,Validated::Invalid]
|
82
|
+
# @return [Validated::Valid,Validated::Invalid]
|
83
|
+
#
|
84
|
+
# @return [Validated::Valid]
|
85
|
+
def apply(val = Undefined)
|
86
|
+
Undefined.default(val) { yield }.fmap(Curry.(value!))
|
87
|
+
end
|
88
|
+
|
89
|
+
# Lifts a block/proc over Valid
|
90
|
+
#
|
91
|
+
# @overload fmap(proc)
|
92
|
+
# @param proc [#call]
|
93
|
+
# @return [Validated::Valid]
|
94
|
+
#
|
95
|
+
# @overload fmap
|
96
|
+
# @param block [Proc]
|
97
|
+
# @return [Validated::Valid]
|
98
|
+
#
|
99
|
+
def fmap(proc = Undefined, &block)
|
100
|
+
f = Undefined.default(proc, block)
|
101
|
+
self.class.new(f.(value!))
|
102
|
+
end
|
103
|
+
|
104
|
+
# Ignores values and returns self, see {Invalid#alt_map}
|
105
|
+
#
|
106
|
+
# @return [Validated::Valid]
|
107
|
+
def alt_map(_ = nil)
|
108
|
+
self
|
109
|
+
end
|
110
|
+
|
111
|
+
# Ignores arguments, returns self
|
112
|
+
#
|
113
|
+
# @return [Validated::Valid]
|
114
|
+
def or(_ = nil)
|
115
|
+
self
|
116
|
+
end
|
117
|
+
|
118
|
+
# @return [String]
|
119
|
+
def inspect
|
120
|
+
"Valid(#{ value!.inspect })"
|
121
|
+
end
|
122
|
+
alias_method :to_s, :inspect
|
123
|
+
|
124
|
+
# Converts to Maybe::Some
|
125
|
+
#
|
126
|
+
# @return [Maybe::Some]
|
127
|
+
def to_maybe
|
128
|
+
Maybe.pure(value!)
|
129
|
+
end
|
130
|
+
|
131
|
+
# Converts to Result::Success
|
132
|
+
#
|
133
|
+
# @return [Result::Success]
|
134
|
+
def to_result
|
135
|
+
Result.pure(value!)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# Invalid result
|
140
|
+
#
|
141
|
+
class Invalid < Validated
|
142
|
+
# The value stored inside
|
143
|
+
#
|
144
|
+
# @return [Object]
|
145
|
+
attr_reader :error
|
146
|
+
|
147
|
+
# Line where the value was constructed
|
148
|
+
#
|
149
|
+
# @return [String]
|
150
|
+
# @api public
|
151
|
+
attr_reader :trace
|
152
|
+
|
153
|
+
include Dry::Equalizer(:error)
|
154
|
+
|
155
|
+
def initialize(error, trace = RightBiased::Left.trace_caller)
|
156
|
+
@error = error
|
157
|
+
@trace = trace
|
158
|
+
end
|
159
|
+
|
160
|
+
# Collects errors (ignores valid results)
|
161
|
+
#
|
162
|
+
# @overload apply(val)
|
163
|
+
# @param val [Validated::Valid,Validated::Invalid]
|
164
|
+
# @return [Validated::Invalid]
|
165
|
+
#
|
166
|
+
# @overload apply
|
167
|
+
# @yieldreturn [Validated::Valid,Validated::Invalid]
|
168
|
+
# @return [Validated::Invalid]
|
169
|
+
#
|
170
|
+
def apply(val = Undefined)
|
171
|
+
Undefined.default(val) { yield }.alt_map { |v| @error + v }
|
172
|
+
end
|
173
|
+
|
174
|
+
# Lifts a block/proc over Invalid
|
175
|
+
#
|
176
|
+
# @overload alt_map(proc)
|
177
|
+
# @param proc [#call]
|
178
|
+
# @return [Validated::Invalid]
|
179
|
+
#
|
180
|
+
# @overload alt_map
|
181
|
+
# @param block [Proc]
|
182
|
+
# @return [Validated::Invalid]
|
183
|
+
#
|
184
|
+
def alt_map(proc = Undefined, &block)
|
185
|
+
f = Undefined.default(proc, block)
|
186
|
+
self.class.new(f.(error), RightBiased::Left.trace_caller)
|
187
|
+
end
|
188
|
+
|
189
|
+
# Ignores the passed argument and returns self
|
190
|
+
#
|
191
|
+
# @return [Validated::Invalid]
|
192
|
+
def fmap(_ = nil)
|
193
|
+
self
|
194
|
+
end
|
195
|
+
|
196
|
+
# Yields the given callable and returns the result
|
197
|
+
#
|
198
|
+
# @overload or(proc)
|
199
|
+
# @param proc [#call]
|
200
|
+
# @return [Object]
|
201
|
+
#
|
202
|
+
# @overload or
|
203
|
+
# @param block [Proc]
|
204
|
+
# @return [Object]
|
205
|
+
#
|
206
|
+
def or(proc = Undefined, &block)
|
207
|
+
Undefined.default(proc, block).call
|
208
|
+
end
|
209
|
+
|
210
|
+
# @return [String]
|
211
|
+
def inspect
|
212
|
+
"Invalid(#{ @error.inspect })"
|
213
|
+
end
|
214
|
+
alias_method :to_s, :inspect
|
215
|
+
|
216
|
+
# Converts to Maybe::None
|
217
|
+
#
|
218
|
+
# @return [Maybe::None]
|
219
|
+
def to_maybe
|
220
|
+
Maybe::None.new(RightBiased::Left.trace_caller)
|
221
|
+
end
|
222
|
+
|
223
|
+
# Concerts to Result::Failure
|
224
|
+
#
|
225
|
+
# @return [Result::Failure]
|
226
|
+
def to_result
|
227
|
+
Result::Failure.new(error, RightBiased::Left.trace_caller)
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
# Mixin with Validated constructors
|
232
|
+
#
|
233
|
+
module Mixin
|
234
|
+
|
235
|
+
# Successful validation result
|
236
|
+
# @see Dry::Monads::Validated::Valid
|
237
|
+
Valid = Valid
|
238
|
+
|
239
|
+
# Unsuccessful validation result
|
240
|
+
# @see Dry::Monads::Validated::Invalid
|
241
|
+
Invalid = Invalid
|
242
|
+
|
243
|
+
# Actual constructor methods
|
244
|
+
#
|
245
|
+
module Constructors
|
246
|
+
# Valid constructor
|
247
|
+
#
|
248
|
+
# @overload Valid(value)
|
249
|
+
# @param value [Object]
|
250
|
+
# @return [Valdated::Valid]
|
251
|
+
#
|
252
|
+
# @overload Valid(&block)
|
253
|
+
# @param block [Proc]
|
254
|
+
# @return [Valdated::Valid]
|
255
|
+
#
|
256
|
+
def Valid(value = Undefined, &block)
|
257
|
+
v = Undefined.default(value, block)
|
258
|
+
raise ArgumentError, 'No value given' if !value.nil? && v.nil?
|
259
|
+
Valid.new(v)
|
260
|
+
end
|
261
|
+
|
262
|
+
# Invalid constructor
|
263
|
+
#
|
264
|
+
# @overload Invalid(value)
|
265
|
+
# @param value [Object]
|
266
|
+
# @return [Valdated::Invalid]
|
267
|
+
#
|
268
|
+
# @overload Invalid(&block)
|
269
|
+
# @param block [Proc]
|
270
|
+
# @return [Valdated::Invalid]
|
271
|
+
#
|
272
|
+
def Invalid(value = Undefined, &block)
|
273
|
+
v = Undefined.default(value, block)
|
274
|
+
raise ArgumentError, 'No value given' if !value.nil? && v.nil?
|
275
|
+
Invalid.new(v, RightBiased::Left.trace_caller)
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
include Constructors
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|