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.
@@ -13,15 +13,25 @@ module Dry
13
13
  class Result
14
14
  include Transformer
15
15
 
16
- attr_reader :success, :failure
16
+ # @return [Object] Successful result
17
+ attr_reader :success
18
+
19
+ # @return [Object] Error
20
+ attr_reader :failure
17
21
 
18
22
  class << self
19
- # Wraps the given value with Success
23
+ # Wraps the given value with Success.
20
24
  #
21
- # @param value [Object] the value to be stored inside Success
22
- # @return [Result::Success]
23
- def pure(value)
24
- Success.new(value)
25
+ # @overload pure(value)
26
+ # @param value [Object]
27
+ # @return [Result::Success]
28
+ #
29
+ # @overload pure(&block)
30
+ # @param block [Proc] a block to be wrapped with Success
31
+ # @return [Result::Success]
32
+ #
33
+ def pure(value = Undefined, &block)
34
+ Success.new(Undefined.default(value, block))
25
35
  end
26
36
  end
27
37
 
@@ -31,7 +41,13 @@ module Dry
31
41
  def to_result
32
42
  self
33
43
  end
34
- alias_method :to_either, :to_result
44
+
45
+ # Returns self.
46
+ #
47
+ # @return [Result::Success, Result::Failure]
48
+ def to_monad
49
+ self
50
+ end
35
51
 
36
52
  # Returns the Result monad.
37
53
  # This is how we're doing polymorphism in Ruby 😕
@@ -66,13 +82,11 @@ module Dry
66
82
  def failure?
67
83
  false
68
84
  end
69
- alias_method :left?, :failure?
70
85
 
71
86
  # Returns true
72
87
  def success?
73
88
  true
74
89
  end
75
- alias_method :right?, :success?
76
90
 
77
91
  # Does the same thing as #bind except it also wraps the value
78
92
  # in an instance of Result::Success monad. This allows for easier
@@ -99,11 +113,18 @@ module Dry
99
113
  Dry::Monads::Maybe(@value)
100
114
  end
101
115
 
102
- # Transform to a Failure instance
116
+ # Transforms to a Failure instance
103
117
  #
104
118
  # @return [Result::Failure]
105
119
  def flip
106
- Failure.new(@value)
120
+ Failure.new(@value, RightBiased::Left.trace_caller)
121
+ end
122
+
123
+ # Transforms to Validated
124
+ #
125
+ # @return [Validated::Valid]
126
+ def to_validated
127
+ Validated::Valid.new(value!)
107
128
  end
108
129
  end
109
130
 
@@ -114,15 +135,22 @@ module Dry
114
135
  include RightBiased::Left
115
136
  include Dry::Equalizer(:failure)
116
137
 
117
- # @api private
118
- def failure
119
- @value
120
- end
121
- alias_method :left, :failure
138
+ # Line where the value was constructed
139
+ #
140
+ # @return [String]
141
+ # @api public
142
+ attr_reader :trace
122
143
 
123
- # @param value [Object] a value in an error state
124
- def initialize(value)
144
+ # @param value [Object] failure value
145
+ # @param trace [String] caller line
146
+ def initialize(value, trace = RightBiased::Left.trace_caller)
125
147
  @value = value
148
+ @trace = trace
149
+ end
150
+
151
+ # @private
152
+ def failure
153
+ @value
126
154
  end
127
155
 
128
156
  # Apply the first function to value.
@@ -136,13 +164,11 @@ module Dry
136
164
  def failure?
137
165
  true
138
166
  end
139
- alias_method :left?, :failure?
140
167
 
141
168
  # Returns false
142
169
  def success?
143
170
  false
144
171
  end
145
- alias_method :right?, :success?
146
172
 
147
173
  # If a block is given passes internal value to it and returns the result,
148
174
  # otherwise simply returns the first argument.
@@ -182,7 +208,7 @@ module Dry
182
208
 
183
209
  # @return [Maybe::None]
184
210
  def to_maybe
185
- Maybe::None.instance
211
+ Maybe::None.new(trace)
186
212
  end
187
213
 
188
214
  # Transform to a Success instance
@@ -192,7 +218,7 @@ module Dry
192
218
  Success.new(@value)
193
219
  end
194
220
 
195
- # @see Dry::Monads::RightBiased::Left#value_or
221
+ # @see RightBiased::Left#value_or
196
222
  def value_or(val = nil)
197
223
  if block_given?
198
224
  yield(@value)
@@ -206,45 +232,63 @@ module Dry
206
232
  def ===(other)
207
233
  Failure === other && failure === other.failure
208
234
  end
235
+
236
+ # Transforms to Validated
237
+ #
238
+ # @return [Validated::Valid]
239
+ def to_validated
240
+ Validated::Invalid.new(failure, trace)
241
+ end
209
242
  end
210
243
 
211
244
  # A module that can be included for easier access to Result monads.
245
+ #
246
+ # @api public
212
247
  module Mixin
213
- Success = Dry::Monads::Result::Success
214
- Failure = Dry::Monads::Result::Failure
248
+ # @see Result::Success
249
+ Success = Result::Success
250
+ # @see Result::Failure
251
+ Failure = Result::Failure
215
252
 
253
+ # Value constructors
254
+ #
216
255
  module Constructors
217
- # @param value [Object] the value to be stored in the monad
218
- # @return [Result::Success]
219
- def Success(value = Dry::Core::Constants::Undefined, &block)
220
- if value.equal?(Dry::Core::Constants::Undefined)
221
- raise ArgumentError, 'No value given' if block.nil?
222
- Success.new(block)
223
- else
224
- Success.new(value)
225
- end
256
+
257
+ # Success constructor
258
+ #
259
+ # @overload Success(value)
260
+ # @param value [Object]
261
+ # @return [Result::Success]
262
+ #
263
+ # @overload Success(&block)
264
+ # @param block [Proc] a block to be wrapped with Success
265
+ # @return [Result::Success]
266
+ #
267
+ def Success(value = Undefined, &block)
268
+ v = Undefined.default(value, block)
269
+ raise ArgumentError, 'No value given' if !value.nil? && v.nil?
270
+ Success.new(v)
226
271
  end
227
- alias_method :Right, :Success
228
-
229
- # @param value [Object] the value to be stored in the monad
230
- # @return [Result::Failure]
231
- def Failure(value = Dry::Core::Constants::Undefined, &block)
232
- if value.equal?(Dry::Core::Constants::Undefined)
233
- raise ArgumentError, 'No value given' if block.nil?
234
- Failure.new(block)
235
- else
236
- Failure.new(value)
237
- end
272
+
273
+ # Failure constructor
274
+ #
275
+ # @overload Success(value)
276
+ # @param value [Object]
277
+ # @return [Result::Failure]
278
+ #
279
+ # @overload Success(&block)
280
+ # @param block [Proc] a block to be wrapped with Failure
281
+ # @return [Result::Failure]
282
+ #
283
+ def Failure(value = Undefined, &block)
284
+ v = Undefined.default(value, block)
285
+ raise ArgumentError, 'No value given' if !value.nil? && v.nil?
286
+ Failure.new(v, RightBiased::Left.trace_caller)
238
287
  end
239
- alias_method :Left, :Failure
240
288
  end
241
289
 
242
290
  include Constructors
243
291
  end
244
292
  end
245
-
246
- Either = Result
247
- Result::Right = Result::Success
248
- Result::Left = Result::Failure
249
293
  end
250
294
  end
@@ -1,7 +1,7 @@
1
1
  module Dry::Monads
2
2
  class Result
3
3
  # @see Monads#Result
4
- # @api private
4
+ # @private
5
5
  class Fixed < Module
6
6
  def self.[](error, **options)
7
7
  new(error, **options)
@@ -11,7 +11,7 @@ module Dry::Monads
11
11
  @mod = Module.new do
12
12
  define_method(:Failure) do |value|
13
13
  if error === value
14
- Failure.new(value)
14
+ Failure.new(value, RightBiased::Left.trace_caller)
15
15
  else
16
16
  raise InvalidFailureTypeError.new(value)
17
17
  end
@@ -1,17 +1,18 @@
1
1
  require 'dry/core/constants'
2
- require 'dry/core/deprecations'
3
2
 
3
+ require 'dry/monads/curry'
4
4
  require 'dry/monads/errors'
5
5
 
6
6
  module Dry
7
7
  module Monads
8
+ # A common module for right-biased monads, such as Result/Either, Maybe, and Try.
8
9
  module RightBiased
10
+ # Right part
11
+ #
9
12
  # @api public
10
13
  module Right
11
14
  include Dry::Core::Constants
12
15
 
13
- extend Dry::Core::Deprecations[:'dry-monads']
14
-
15
16
  # Unwraps the underlying value
16
17
  #
17
18
  # @return [Object]
@@ -19,8 +20,6 @@ module Dry
19
20
  @value
20
21
  end
21
22
 
22
- deprecate :value, :value!
23
-
24
23
  # Calls the passed in Proc object with value stored in self
25
24
  # and returns the result.
26
25
  #
@@ -65,8 +64,8 @@ module Dry
65
64
  bind(*args, &block).bind { self }
66
65
  end
67
66
 
68
- # Abstract method for lifting a block over the monad type
69
- # Must be implemented for a right-biased monad
67
+ # Abstract method for lifting a block over the monad type.
68
+ # Must be implemented for a right-biased monad.
70
69
  #
71
70
  # @return [RightBiased::Right]
72
71
  def fmap(*)
@@ -97,7 +96,7 @@ module Dry
97
96
  end
98
97
 
99
98
  # Applies the stored value to the given argument if the argument has type of Right,
100
- # otherwise returns the argument
99
+ # otherwise returns the argument.
101
100
  #
102
101
  # @example happy path
103
102
  # create_user = Dry::Monads::Right(CreateUser.new)
@@ -109,11 +108,12 @@ module Dry
109
108
  # create_user.apply(name) # => Left(:name_missing)
110
109
  #
111
110
  # @return [RightBiased::Left,RightBiased::Right]
112
- def apply(val)
111
+ def apply(val = Undefined)
113
112
  unless @value.respond_to?(:call)
114
113
  raise TypeError, "Cannot apply #{ val.inspect } to #{ @value.inspect }"
115
114
  end
116
- val.fmap { |unwrapped| curry.(unwrapped) }
115
+
116
+ Undefined.default(val) { yield }.fmap { |unwrapped| curry.(unwrapped) }
117
117
  end
118
118
 
119
119
  # @param other [RightBiased]
@@ -131,28 +131,19 @@ module Dry
131
131
 
132
132
  # @api private
133
133
  def curry
134
- @curried ||=
135
- begin
136
- func = @value.is_a?(Proc) ? @value : @value.method(:call)
137
- seq_args = func.parameters.count { |type, _| type == :req }
138
- seq_args += 1 if func.parameters.any? { |type, _| type == :keyreq }
139
-
140
- if seq_args > 1
141
- func.curry
142
- else
143
- func
144
- end
145
- end
134
+ @curried ||= Curry.(@value)
146
135
  end
147
136
  end
148
137
 
138
+ # Left/wrong/erroneous part
139
+ #
149
140
  # @api public
150
141
  module Left
151
- extend Dry::Core::Deprecations[:'dry-monads']
152
-
153
- attr_reader :value
154
-
155
- deprecate :value, message: '.value is deprecated, use .value! instead'
142
+ # @private
143
+ # @return [String] Caller location
144
+ def self.trace_caller
145
+ caller_locations(2, 2)[0].to_s
146
+ end
156
147
 
157
148
  # Raises an error on accessing internal value
158
149
  def value!
@@ -0,0 +1,309 @@
1
+ require 'concurrent/promise'
2
+
3
+ require 'dry/monads/curry'
4
+
5
+ module Dry
6
+ module Monads
7
+ # The Task monad represents an async computation. The implementation
8
+ # is a rather thin wrapper of Concurrent::Promise from the concurrent-ruby.
9
+ # The API supports setting a custom executor from concurrent-ruby.
10
+ #
11
+ # @api public
12
+ class Task
13
+ # @api private
14
+ class Promise < Concurrent::Promise
15
+ public :on_fulfill, :on_reject
16
+ end
17
+ private_constant :Promise
18
+
19
+ class << self
20
+ # Creates a Task from a block
21
+ #
22
+ # @overload new(promise)
23
+ # @param promise [Promise]
24
+ # @return [Task]
25
+ #
26
+ # @overload new(&block)
27
+ # @param block [Proc] a task to run
28
+ # @return [Task]
29
+ #
30
+ def new(promise = nil, &block)
31
+ if promise
32
+ super(promise)
33
+ else
34
+ super(Promise.execute(&block))
35
+ end
36
+ end
37
+
38
+ # Creates a Task with the given executor
39
+ #
40
+ # @example providing an executor instance, using Ruby 2.5+ syntax
41
+ # IO = Concurrent::ThreadPoolExecutor.new
42
+ # Task[IO] { do_http_request }
43
+ #
44
+ # @example using a predefined executor
45
+ # Task[:fast] { do_quick_task }
46
+ #
47
+ # @param executor [Concurrent::AbstractExecutorService,Symbol] Either an executor instance
48
+ # or a name of predefined global
49
+ # from concurrent-ruby
50
+ # @return [Task]
51
+ def [](executor, &block)
52
+ new(Promise.execute(executor: executor, &block))
53
+ end
54
+
55
+ # Returns a complete task from the given value
56
+ #
57
+ # @overload pure(value)
58
+ # @param value [Object]
59
+ # @return [Task]
60
+ #
61
+ # @overload pure(&block)
62
+ # @param block [Proc]
63
+ # @return [Task]
64
+ #
65
+ def pure(value = Undefined, &block)
66
+ v = Undefined.default(value, block)
67
+ new(Promise.fulfill(v))
68
+ end
69
+ end
70
+
71
+ # @api private
72
+ attr_reader :promise
73
+ protected :promise
74
+
75
+ # @api private
76
+ def initialize(promise)
77
+ @promise = promise
78
+ end
79
+
80
+ # Retrieves the value of the computation.
81
+ # Blocks current thread if the underlying promise
82
+ # hasn't been complete yet.
83
+ # Throws an error if the computation failed.
84
+ #
85
+ # @return [Object]
86
+ # @api public
87
+ def value!
88
+ if promise.wait.fulfilled?
89
+ promise.value
90
+ else
91
+ raise promise.reason
92
+ end
93
+ end
94
+
95
+ # Lifts a block over the Task monad.
96
+ #
97
+ # @param block [Proc]
98
+ # @return [Task]
99
+ # @api public
100
+ def fmap(&block)
101
+ self.class.new(promise.then(&block))
102
+ end
103
+
104
+ # Composes two tasks to run one after another.
105
+ # A more common name is `then` exists as an alias.
106
+ #
107
+ # @param block [Proc] A block that yields the result of the current task
108
+ # and returns another task
109
+ # @return [Task]
110
+ def bind(&block)
111
+ self.class.new(promise.flat_map { |value| block.(value).promise })
112
+ end
113
+ alias_method :then, :bind
114
+
115
+ # Converts to Result. Blocks the current thread if required.
116
+ #
117
+ # @return [Result]
118
+ def to_result
119
+ if promise.wait.fulfilled?
120
+ Result::Success.new(promise.value)
121
+ else
122
+ Result::Failure.new(promise.reason, RightBiased::Left.trace_caller)
123
+ end
124
+ end
125
+
126
+ # Converts to Maybe. Blocks the current thread if required.
127
+ #
128
+ # @return [Maybe]
129
+ def to_maybe
130
+ if promise.wait.fulfilled?
131
+ Maybe::Some.new(promise.value)
132
+ else
133
+ Maybe::None.new(RightBiased::Left.trace_caller)
134
+ end
135
+ end
136
+
137
+ # @return [String]
138
+ def to_s
139
+ state = case promise.state
140
+ when :fulfilled
141
+ "value=#{ value!.inspect }"
142
+ when :rejected
143
+ "error=#{ promise.reason.inspect }"
144
+ else
145
+ '?'
146
+ end
147
+
148
+ "Task(#{ state })"
149
+ end
150
+ alias_method :inspect, :to_s
151
+
152
+ # Tranforms the error if the computation wasn't successful.
153
+ #
154
+ # @param block [Proc]
155
+ # @return [Task]
156
+ def or_fmap(&block)
157
+ self.class.new(promise.rescue(&block))
158
+ end
159
+
160
+ # Rescues the error with a block that returns another task.
161
+ #
162
+ # @param block [Proc]
163
+ # @return [Object]
164
+ def or(&block)
165
+ child = Promise.new(
166
+ parent: promise,
167
+ executor: Concurrent::ImmediateExecutor.new
168
+ )
169
+
170
+ promise.on_error do |v|
171
+ begin
172
+ inner = block.(v).promise
173
+ inner.execute
174
+ inner.on_success { |r| child.on_fulfill(r) }
175
+ inner.on_error { |e| child.on_reject(e) }
176
+ rescue => e
177
+ child.on_reject(e)
178
+ end
179
+ end
180
+ promise.on_success { |v| child.on_fulfill(v) }
181
+
182
+ self.class.new(child)
183
+ end
184
+
185
+ # Extracts the resulting value if the computation was successful
186
+ # otherwise yields the block and returns its result.
187
+ #
188
+ # @param block [Proc]
189
+ # @return [Object]
190
+ def value_or(&block)
191
+ promise.rescue(&block).wait.value
192
+ end
193
+
194
+ # Blocks the current thread until the task is complete.
195
+ #
196
+ # @return [Task]
197
+ def wait(timeout = nil)
198
+ promise.wait(timeout)
199
+ self
200
+ end
201
+
202
+ # Compares two tasks. Note, it works
203
+ # good enough only for complete tasks.
204
+ #
205
+ # @return [Boolean]
206
+ def ==(other)
207
+ return true if equal?(other)
208
+ return false unless self.class == other.class
209
+ compare_promises(promise, other.promise)
210
+ end
211
+
212
+ # Whether the computation is complete.
213
+ #
214
+ # @return [Boolean]
215
+ def complete?
216
+ promise.complete?
217
+ end
218
+
219
+ # @return [Class]
220
+ def monad
221
+ Task
222
+ end
223
+
224
+ # Returns self.
225
+ #
226
+ # @return [Maybe::Some, Maybe::None]
227
+ def to_monad
228
+ self
229
+ end
230
+
231
+ # Applies the stored value to the given argument.
232
+ #
233
+ # @example
234
+ # Task.
235
+ # pure { |x, y| x ** y }.
236
+ # apply(Task { 2 }).
237
+ # apply(Task { 3 }).
238
+ # to_maybe # => Some(8)
239
+ #
240
+ # @param val [Task]
241
+ # @return [Task]
242
+ def apply(val = Undefined)
243
+ arg = Undefined.default(val) { yield }
244
+ bind { |f| arg.fmap { |v| curry(f).(v) } }
245
+ end
246
+
247
+ private
248
+
249
+ # @api private
250
+ def curry(value)
251
+ if defined?(@curried)
252
+ if @curried[0].equal?(value)
253
+ @curried[1]
254
+ else
255
+ Curry.(value)
256
+ end
257
+ else
258
+ @curried = [value, Curry.(value)]
259
+ @curried[1]
260
+ end
261
+ end
262
+
263
+ # @api private
264
+ def compare_promises(x, y)
265
+ x.equal?(y) ||
266
+ x.fulfilled? && y.fulfilled? && x.value == y.value ||
267
+ x.rejected? && y.rejected? && x.reason == y.reason
268
+ end
269
+
270
+ # Task constructors.
271
+ #
272
+ # @api public
273
+ module Mixin
274
+ Task = Task # @private
275
+
276
+ # Created a mixin with the given executor injected.
277
+ #
278
+ # @param executor [Concurrent::AbstractExecutorService,Symbol]
279
+ # @return [Module]
280
+ def self.[](executor)
281
+ Module.new do
282
+ include Mixin
283
+
284
+ # Created a new Task with an injected executor.
285
+ #
286
+ # @param block [Proc]
287
+ # @return [Task]
288
+ define_method(:Task) do |&block|
289
+ Task[executor, &block]
290
+ end
291
+ end
292
+ end
293
+
294
+ # Task constructors
295
+ module Constructors
296
+ # Builds a new Task instance.
297
+ #
298
+ # @param block [Proc]
299
+ # @return Task
300
+ def Task(&block)
301
+ Task.new(&block)
302
+ end
303
+ end
304
+
305
+ include Constructors
306
+ end
307
+ end
308
+ end
309
+ end