dry-monads 1.3.2 → 1.4.0
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 +4 -4
- data/CHANGELOG.md +157 -73
- data/LICENSE +1 -1
- data/README.md +18 -38
- data/dry-monads.gemspec +32 -30
- data/lib/dry-monads.rb +3 -1
- data/lib/dry/monads.rb +4 -2
- data/lib/dry/monads/all.rb +4 -2
- data/lib/dry/monads/constants.rb +1 -1
- data/lib/dry/monads/conversion_stubs.rb +2 -0
- data/lib/dry/monads/curry.rb +2 -0
- data/lib/dry/monads/do.rb +55 -17
- data/lib/dry/monads/do/all.rb +39 -17
- data/lib/dry/monads/do/mixin.rb +2 -0
- data/lib/dry/monads/either.rb +9 -7
- data/lib/dry/monads/errors.rb +8 -3
- data/lib/dry/monads/lazy.rb +19 -6
- data/lib/dry/monads/list.rb +31 -30
- data/lib/dry/monads/maybe.rb +90 -19
- data/lib/dry/monads/registry.rb +15 -12
- data/lib/dry/monads/result.rb +42 -15
- data/lib/dry/monads/result/fixed.rb +35 -24
- data/lib/dry/monads/right_biased.rb +45 -24
- data/lib/dry/monads/task.rb +25 -22
- data/lib/dry/monads/transformer.rb +4 -1
- data/lib/dry/monads/traverse.rb +9 -1
- data/lib/dry/monads/try.rb +51 -13
- data/lib/dry/monads/unit.rb +6 -2
- data/lib/dry/monads/validated.rb +27 -20
- data/lib/dry/monads/version.rb +3 -1
- data/lib/json/add/dry/monads/maybe.rb +4 -3
- metadata +27 -75
- data/.codeclimate.yml +0 -12
- data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +0 -10
- data/.github/ISSUE_TEMPLATE/---bug-report.md +0 -34
- data/.github/ISSUE_TEMPLATE/---feature-request.md +0 -18
- data/.github/workflows/ci.yml +0 -74
- data/.github/workflows/docsite.yml +0 -34
- data/.github/workflows/sync_configs.yml +0 -34
- data/.gitignore +0 -10
- data/.rspec +0 -4
- data/.rubocop.yml +0 -89
- data/.yardopts +0 -4
- data/CODE_OF_CONDUCT.md +0 -13
- data/CONTRIBUTING.md +0 -29
- data/Gemfile +0 -23
- data/Rakefile +0 -6
- data/bin/console +0 -16
- data/bin/setup +0 -7
- data/docsite/source/case-equality.html.md +0 -42
- data/docsite/source/do-notation.html.md +0 -207
- data/docsite/source/getting-started.html.md +0 -142
- data/docsite/source/index.html.md +0 -179
- data/docsite/source/list.html.md +0 -87
- data/docsite/source/maybe.html.md +0 -146
- data/docsite/source/pattern-matching.html.md +0 -68
- data/docsite/source/result.html.md +0 -190
- data/docsite/source/task.html.md +0 -126
- data/docsite/source/tracing-failures.html.md +0 -32
- data/docsite/source/try.html.md +0 -76
- data/docsite/source/unit.html.md +0 -36
- data/docsite/source/validated.html.md +0 -88
- data/log/.gitkeep +0 -0
data/lib/dry/monads/errors.rb
CHANGED
@@ -1,16 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Dry
|
2
4
|
module Monads
|
3
5
|
# An unsuccessful result of extracting a value from a monad.
|
4
6
|
class UnwrapError < StandardError
|
5
|
-
|
6
|
-
|
7
|
+
attr_reader :receiver
|
8
|
+
|
9
|
+
def initialize(receiver)
|
10
|
+
@receiver = receiver
|
11
|
+
super("value! was called on #{receiver.inspect}")
|
7
12
|
end
|
8
13
|
end
|
9
14
|
|
10
15
|
# An error thrown on returning a Failure of unknown type.
|
11
16
|
class InvalidFailureTypeError < StandardError
|
12
17
|
def initialize(failure)
|
13
|
-
super("Cannot create Failure from #{
|
18
|
+
super("Cannot create Failure from #{failure.inspect}, it doesn't meet the constraints")
|
14
19
|
end
|
15
20
|
end
|
16
21
|
|
data/lib/dry/monads/lazy.rb
CHANGED
@@ -1,6 +1,9 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "concurrent/promise"
|
4
|
+
|
5
|
+
require "dry/core/deprecations"
|
6
|
+
require "dry/monads/task"
|
4
7
|
|
5
8
|
module Dry
|
6
9
|
module Monads
|
@@ -9,6 +12,8 @@ module Dry
|
|
9
12
|
# computation is evaluated not more than once (compare with the built-in
|
10
13
|
# lazy assignement ||= which does not guarantee this).
|
11
14
|
class Lazy < Task
|
15
|
+
extend ::Dry::Core::Deprecations[:"dry-monads"]
|
16
|
+
|
12
17
|
class << self
|
13
18
|
# @private
|
14
19
|
def new(promise = nil, &block)
|
@@ -39,18 +44,26 @@ module Dry
|
|
39
44
|
self
|
40
45
|
end
|
41
46
|
|
47
|
+
# @return [Boolean]
|
48
|
+
def evaluated?
|
49
|
+
@promise.complete?
|
50
|
+
end
|
51
|
+
deprecate :complete?, :evaluated?
|
52
|
+
|
53
|
+
undef_method :wait
|
54
|
+
|
42
55
|
# @return [String]
|
43
56
|
def to_s
|
44
57
|
state = case promise.state
|
45
58
|
when :fulfilled
|
46
59
|
value!.inspect
|
47
60
|
when :rejected
|
48
|
-
"!#{
|
61
|
+
"!#{promise.reason.inspect}"
|
49
62
|
else
|
50
|
-
|
63
|
+
"?"
|
51
64
|
end
|
52
65
|
|
53
|
-
"Lazy(#{
|
66
|
+
"Lazy(#{state})"
|
54
67
|
end
|
55
68
|
alias_method :inspect, :to_s
|
56
69
|
|
@@ -78,7 +91,7 @@ module Dry
|
|
78
91
|
end
|
79
92
|
end
|
80
93
|
|
81
|
-
require
|
94
|
+
require "dry/monads/registry"
|
82
95
|
register_mixin(:lazy, Lazy::Mixin)
|
83
96
|
end
|
84
97
|
end
|
data/lib/dry/monads/list.rb
CHANGED
@@ -1,12 +1,14 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
8
|
-
require
|
9
|
-
require
|
3
|
+
require "dry/core/equalizer"
|
4
|
+
|
5
|
+
require "dry/monads/maybe"
|
6
|
+
require "dry/monads/task"
|
7
|
+
require "dry/monads/result"
|
8
|
+
require "dry/monads/try"
|
9
|
+
require "dry/monads/validated"
|
10
|
+
require "dry/monads/transformer"
|
11
|
+
require "dry/monads/curry"
|
10
12
|
|
11
13
|
module Dry
|
12
14
|
module Monads
|
@@ -81,7 +83,7 @@ module Dry
|
|
81
83
|
end
|
82
84
|
end
|
83
85
|
|
84
|
-
extend Dry::Core::Deprecations[:
|
86
|
+
extend Dry::Core::Deprecations[:"dry-monads"]
|
85
87
|
|
86
88
|
include Dry::Equalizer(:value, :type)
|
87
89
|
include Transformer
|
@@ -134,14 +136,14 @@ module Dry
|
|
134
136
|
end
|
135
137
|
|
136
138
|
# Maps a block over the list. Acts as `Array#map`.
|
137
|
-
#
|
139
|
+
# If called without a block, this method returns an enumerator, not a List
|
138
140
|
#
|
139
141
|
# @return [List,Enumerator]
|
140
142
|
def map(&block)
|
141
|
-
if
|
143
|
+
if block_given?
|
142
144
|
fmap(block)
|
143
145
|
else
|
144
|
-
value.map
|
146
|
+
value.map
|
145
147
|
end
|
146
148
|
end
|
147
149
|
|
@@ -163,8 +165,8 @@ module Dry
|
|
163
165
|
#
|
164
166
|
# @return [String]
|
165
167
|
def inspect
|
166
|
-
type_ann = typed? ? "<#{
|
167
|
-
"List#{
|
168
|
+
type_ann = typed? ? "<#{type.name.split("::").last}>" : ""
|
169
|
+
"List#{type_ann}#{value.inspect}"
|
168
170
|
end
|
169
171
|
alias_method :to_s, :inspect
|
170
172
|
|
@@ -190,8 +192,8 @@ module Dry
|
|
190
192
|
#
|
191
193
|
# @param initial [Object] Initial value
|
192
194
|
# @return [Object]
|
193
|
-
def fold_left(initial)
|
194
|
-
value.reduce(initial
|
195
|
+
def fold_left(initial, &block)
|
196
|
+
value.reduce(initial, &block)
|
195
197
|
end
|
196
198
|
alias_method :foldl, :fold_left
|
197
199
|
alias_method :reduce, :fold_left
|
@@ -222,8 +224,8 @@ module Dry
|
|
222
224
|
# Filters elements with a block
|
223
225
|
#
|
224
226
|
# @return [List]
|
225
|
-
def filter
|
226
|
-
coerce(value.select
|
227
|
+
def filter(&block)
|
228
|
+
coerce(value.select(&block))
|
227
229
|
end
|
228
230
|
alias_method :select, :filter
|
229
231
|
|
@@ -303,9 +305,9 @@ module Dry
|
|
303
305
|
with = proc || block || Traverse[type]
|
304
306
|
|
305
307
|
foldl(type.pure(EMPTY)) do |acc, el|
|
306
|
-
cons
|
307
|
-
apply(acc)
|
308
|
-
apply { with.(el) }
|
308
|
+
cons
|
309
|
+
.apply(acc)
|
310
|
+
.apply { with.(el) }
|
309
311
|
end
|
310
312
|
end
|
311
313
|
|
@@ -313,8 +315,8 @@ module Dry
|
|
313
315
|
#
|
314
316
|
# @param list [List]
|
315
317
|
# @return [List]
|
316
|
-
def apply(list = Undefined)
|
317
|
-
v = Undefined.default(list)
|
318
|
+
def apply(list = Undefined, &block)
|
319
|
+
v = Undefined.default(list, &block)
|
318
320
|
fmap(Curry).bind { |f| v.fmap { |x| f.(x) } }
|
319
321
|
end
|
320
322
|
|
@@ -327,7 +329,7 @@ module Dry
|
|
327
329
|
|
328
330
|
# Returns self.
|
329
331
|
#
|
330
|
-
# @return [
|
332
|
+
# @return [List]
|
331
333
|
def to_monad
|
332
334
|
self
|
333
335
|
end
|
@@ -343,7 +345,7 @@ module Dry
|
|
343
345
|
# Some(n / divisor)
|
344
346
|
# end
|
345
347
|
# end
|
346
|
-
# # => List[
|
348
|
+
# # => List[2, 4]
|
347
349
|
#
|
348
350
|
# @example without block
|
349
351
|
# List[Some(5), None(), Some(3)].collect.map { |x| x * 2 }
|
@@ -420,10 +422,10 @@ module Dry
|
|
420
422
|
# List of results
|
421
423
|
Result = ListBuilder[Result]
|
422
424
|
|
423
|
-
# List of
|
425
|
+
# List of maybes
|
424
426
|
Maybe = ListBuilder[Maybe]
|
425
427
|
|
426
|
-
# List of
|
428
|
+
# List of tries
|
427
429
|
Try = ListBuilder[Try]
|
428
430
|
|
429
431
|
# List of validation results
|
@@ -433,7 +435,6 @@ module Dry
|
|
433
435
|
#
|
434
436
|
# @api public
|
435
437
|
module Mixin
|
436
|
-
|
437
438
|
# @see Dry::Monads::List
|
438
439
|
List = List
|
439
440
|
|
@@ -448,9 +449,9 @@ module Dry
|
|
448
449
|
end
|
449
450
|
end
|
450
451
|
|
451
|
-
require
|
452
|
+
require "dry/monads/registry"
|
452
453
|
register_mixin(:list, List::Mixin)
|
453
454
|
end
|
454
455
|
end
|
455
456
|
|
456
|
-
require
|
457
|
+
require "dry/monads/traverse"
|
data/lib/dry/monads/maybe.rb
CHANGED
@@ -1,10 +1,13 @@
|
|
1
|
-
|
2
|
-
require 'dry/core/deprecations'
|
1
|
+
# frozen_string_literal: true
|
3
2
|
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
|
3
|
+
require "dry/core/equalizer"
|
4
|
+
require "dry/core/deprecations"
|
5
|
+
require "dry/core/class_attributes"
|
6
|
+
|
7
|
+
require "dry/monads/right_biased"
|
8
|
+
require "dry/monads/transformer"
|
9
|
+
require "dry/monads/unit"
|
10
|
+
require "dry/monads/constants"
|
8
11
|
|
9
12
|
module Dry
|
10
13
|
module Monads
|
@@ -13,9 +16,13 @@ module Dry
|
|
13
16
|
# @api public
|
14
17
|
class Maybe
|
15
18
|
include Transformer
|
19
|
+
extend Core::ClassAttributes
|
20
|
+
|
21
|
+
defines :warn_on_implicit_nil_coercion
|
22
|
+
warn_on_implicit_nil_coercion true
|
16
23
|
|
17
24
|
class << self
|
18
|
-
extend Core::Deprecations[:
|
25
|
+
extend Core::Deprecations[:"dry-monads"]
|
19
26
|
|
20
27
|
# Wraps the given value with into a Maybe object.
|
21
28
|
#
|
@@ -103,7 +110,10 @@ module Dry
|
|
103
110
|
end
|
104
111
|
|
105
112
|
def initialize(value = Undefined)
|
106
|
-
raise ArgumentError,
|
113
|
+
raise ArgumentError, "nil cannot be some" if value.nil?
|
114
|
+
|
115
|
+
super()
|
116
|
+
|
107
117
|
@value = Undefined.default(value, Unit)
|
108
118
|
end
|
109
119
|
|
@@ -118,14 +128,65 @@ module Dry
|
|
118
128
|
# @return [Maybe::Some, Maybe::None] Wrapped result, i.e. nil will be mapped to None,
|
119
129
|
# other values will be wrapped with Some
|
120
130
|
def fmap(*args, &block)
|
131
|
+
next_value = bind(*args, &block)
|
132
|
+
|
133
|
+
if next_value.nil?
|
134
|
+
if self.class.warn_on_implicit_nil_coercion
|
135
|
+
Core::Deprecations.warn(
|
136
|
+
"Block passed to Some#fmap returned `nil` and was chained to None. "\
|
137
|
+
"This is literally an unlawful behavior and it will not be supported in "\
|
138
|
+
"dry-monads 2. \nPlease, replace `.fmap` with `.maybe` in places where you "\
|
139
|
+
"expect `nil` as block result.\n"\
|
140
|
+
"You can opt out of these warnings with\n"\
|
141
|
+
"Dry::Monads::Maybe.warn_on_implicit_nil_coercion false",
|
142
|
+
uplevel: 0,
|
143
|
+
tag: :'dry-monads'
|
144
|
+
)
|
145
|
+
end
|
146
|
+
Monads.None()
|
147
|
+
else
|
148
|
+
Some.new(next_value)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# Does the same thing as #bind except it also wraps the value
|
153
|
+
# in an instance of the Maybe monad. This allows for easier
|
154
|
+
# chaining of calls.
|
155
|
+
#
|
156
|
+
# @example
|
157
|
+
# Dry::Monads.Some(4).maybe(&:succ).maybe(->(n) { n**2 }) # => Some(25)
|
158
|
+
# Dry::Monads.Some(4).maybe(&:succ).maybe(->(_) { nil }) # => None()
|
159
|
+
#
|
160
|
+
# @param args [Array<Object>] arguments will be transparently passed through to #bind
|
161
|
+
# @return [Maybe::Some, Maybe::None] Wrapped result, i.e. nil will be mapped to None,
|
162
|
+
# other values will be wrapped with Some
|
163
|
+
def maybe(*args, &block)
|
121
164
|
Maybe.coerce(bind(*args, &block))
|
122
165
|
end
|
123
|
-
|
166
|
+
|
167
|
+
# Accepts a block and runs it against the wrapped value.
|
168
|
+
# If the block returns a trurhy value the result is self,
|
169
|
+
# otherwise None. If no block is given, the value serves
|
170
|
+
# and its result.
|
171
|
+
#
|
172
|
+
# @param with [#call] positional block
|
173
|
+
# @param block [Proc] block
|
174
|
+
#
|
175
|
+
# @return [Maybe::None, Maybe::Some]
|
176
|
+
def filter(with = Undefined, &block)
|
177
|
+
block = Undefined.default(with, block || IDENTITY)
|
178
|
+
|
179
|
+
if block.(@value)
|
180
|
+
self
|
181
|
+
else
|
182
|
+
Monads.None()
|
183
|
+
end
|
184
|
+
end
|
124
185
|
|
125
186
|
# @return [String]
|
126
187
|
def to_s
|
127
188
|
if Unit.equal?(@value)
|
128
|
-
|
189
|
+
"Some()"
|
129
190
|
else
|
130
191
|
"Some(#{@value.inspect})"
|
131
192
|
end
|
@@ -143,7 +204,7 @@ module Dry
|
|
143
204
|
singleton_class.send(:attr_reader, :instance)
|
144
205
|
|
145
206
|
# @api private
|
146
|
-
def self.method_missing(m, *)
|
207
|
+
def self.method_missing(m, *) # rubocop:disable Style/MissingRespondToMissing
|
147
208
|
if (instance.methods(true) - methods(true)).include?(m)
|
148
209
|
raise ConstructorNotAppliedError.new(m, :None)
|
149
210
|
else
|
@@ -158,6 +219,7 @@ module Dry
|
|
158
219
|
attr_reader :trace
|
159
220
|
|
160
221
|
def initialize(trace = RightBiased::Left.trace_caller)
|
222
|
+
super()
|
161
223
|
@trace = trace
|
162
224
|
end
|
163
225
|
|
@@ -199,7 +261,7 @@ module Dry
|
|
199
261
|
|
200
262
|
# @return [String]
|
201
263
|
def to_s
|
202
|
-
|
264
|
+
"None"
|
203
265
|
end
|
204
266
|
alias_method :inspect, :to_s
|
205
267
|
|
@@ -227,6 +289,13 @@ module Dry
|
|
227
289
|
def deconstruct
|
228
290
|
EMPTY_ARRAY
|
229
291
|
end
|
292
|
+
|
293
|
+
# @see Maybe::Some#filter
|
294
|
+
#
|
295
|
+
# @return [Maybe::None]
|
296
|
+
def filter(_ = Undefined)
|
297
|
+
self
|
298
|
+
end
|
230
299
|
end
|
231
300
|
|
232
301
|
# A module that can be included for easier access to Maybe monads.
|
@@ -298,9 +367,9 @@ module Dry
|
|
298
367
|
# None values are removed
|
299
368
|
#
|
300
369
|
# @example
|
301
|
-
# Maybe::Hash.filter(foo: Some(1), bar: Some(2)) # =>
|
302
|
-
# Maybe::Hash.filter(foo: Some(1), bar: None()) # =>
|
303
|
-
# Maybe::Hash.filter(foo: None(), bar: Some(2)) # =>
|
370
|
+
# Maybe::Hash.filter(foo: Some(1), bar: Some(2)) # => { foo: 1, bar: 2 }
|
371
|
+
# Maybe::Hash.filter(foo: Some(1), bar: None()) # => { foo: 1 }
|
372
|
+
# Maybe::Hash.filter(foo: None(), bar: Some(2)) # => { bar: 2 }
|
304
373
|
#
|
305
374
|
# @param hash [::Hash<Object,Maybe>]
|
306
375
|
# @return [::Hash]
|
@@ -322,10 +391,12 @@ module Dry
|
|
322
391
|
|
323
392
|
class Result
|
324
393
|
class Success < Result
|
325
|
-
|
394
|
+
extend Core::Deprecations[:"dry-monads"]
|
395
|
+
|
396
|
+
# @return [Maybe]
|
326
397
|
def to_maybe
|
327
|
-
|
328
|
-
Dry::Monads::Maybe(@value)
|
398
|
+
warn "Success(nil) transformed to None" if @value.nil?
|
399
|
+
::Dry::Monads::Maybe(@value)
|
329
400
|
end
|
330
401
|
end
|
331
402
|
|
@@ -386,7 +457,7 @@ module Dry
|
|
386
457
|
end
|
387
458
|
end
|
388
459
|
|
389
|
-
require
|
460
|
+
require "dry/monads/registry"
|
390
461
|
register_mixin(:maybe, Maybe::Mixin)
|
391
462
|
end
|
392
463
|
end
|
data/lib/dry/monads/registry.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "concurrent/map"
|
2
4
|
|
3
5
|
module Dry
|
4
6
|
# Common, idiomatic monads for Ruby
|
@@ -8,16 +10,16 @@ module Dry
|
|
8
10
|
@registry = {}
|
9
11
|
@constructors = nil
|
10
12
|
@paths = {
|
11
|
-
do:
|
12
|
-
lazy:
|
13
|
-
list:
|
14
|
-
maybe:
|
15
|
-
task:
|
16
|
-
try:
|
17
|
-
validated:
|
13
|
+
do: "dry/monads/do/all",
|
14
|
+
lazy: "dry/monads/lazy",
|
15
|
+
list: "dry/monads/list",
|
16
|
+
maybe: "dry/monads/maybe",
|
17
|
+
task: "dry/monads/task",
|
18
|
+
try: "dry/monads/try",
|
19
|
+
validated: "dry/monads/validated",
|
18
20
|
result: [
|
19
|
-
|
20
|
-
|
21
|
+
"dry/monads/result",
|
22
|
+
"dry/monads/result/fixed"
|
21
23
|
]
|
22
24
|
}.freeze
|
23
25
|
@mixins = Concurrent::Map.new
|
@@ -36,8 +38,9 @@ module Dry
|
|
36
38
|
# @private
|
37
39
|
def register_mixin(name, mod)
|
38
40
|
if registry.key?(name)
|
39
|
-
raise ArgumentError, "#{
|
41
|
+
raise ArgumentError, "#{name.inspect} is already registered"
|
40
42
|
end
|
43
|
+
|
41
44
|
self.registry = registry.merge(name => mod)
|
42
45
|
end
|
43
46
|
|
@@ -49,7 +52,7 @@ module Dry
|
|
49
52
|
# @private
|
50
53
|
def load_monad(name)
|
51
54
|
path = @paths.fetch(name) {
|
52
|
-
raise ArgumentError, "#{
|
55
|
+
raise ArgumentError, "#{name.inspect} is not a known monad"
|
53
56
|
}
|
54
57
|
Array(path).each { |p| require p }
|
55
58
|
end
|