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