dry-monads 0.4.0 → 1.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,6 @@
1
1
  module Dry
2
2
  module Monads
3
+ # Advanced tranformations.
3
4
  module Transformer
4
5
  # Lifts a block/proc over the 2-level nested structure.
5
6
  # This is essentially fmap . fmap (. is the function composition
@@ -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
@@ -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
- # Calls the passed in proc object and if successful stores the result in a
17
- # {Try::Value} monad, but if one of the specified exceptions was raised it stores
18
- # it in a {Try::Error} monad.
19
- #
20
- # @param exceptions [Array<Exception>] list of exceptions to be rescued
21
- # @param f [Proc] the proc to be called
22
- # @return [Try::Value, Try::Error]
23
- def self.lift(exceptions, f)
24
- Value.new(exceptions, f.call)
25
- rescue *exceptions => e
26
- Error.new(e)
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
- Dry::Monads::None()
187
+ Maybe::None.new(RightBiased::Left.trace_caller)
133
188
  end
134
189
 
135
190
  # @return [Result::Failure]
136
191
  def to_result
137
- Dry::Monads::Failure(exception)
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
- DEFAULT_EXCEPTIONS = [StandardError].freeze
191
-
192
- # A convenience wrapper for {Monads::Try.lift}.
193
- # If no exceptions are provided it falls back to StandardError.
194
- # In general, relying on this behaviour is not recommended as it can lead to unnoticed
195
- # bugs and it is always better to explicitly specify a list of exceptions if possible.
196
- #
197
- # @param exceptions [Array<Exception>]
198
- def Try(*exceptions, &f)
199
- catchable = exceptions.empty? ? DEFAULT_EXCEPTIONS : exceptions.flatten
200
- Try.lift(catchable, f)
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
- if value.equal?(Undefined)
205
- raise ArgumentError, 'No value given' if block.nil?
206
- Try::Value.new(exceptions, block)
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
- if error.equal?(Undefined)
214
- raise ArgumentError, 'No value given' if block.nil?
215
- Try::Error.new(block)
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