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