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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +157 -73
  3. data/LICENSE +1 -1
  4. data/README.md +18 -38
  5. data/dry-monads.gemspec +32 -30
  6. data/lib/dry-monads.rb +3 -1
  7. data/lib/dry/monads.rb +4 -2
  8. data/lib/dry/monads/all.rb +4 -2
  9. data/lib/dry/monads/constants.rb +1 -1
  10. data/lib/dry/monads/conversion_stubs.rb +2 -0
  11. data/lib/dry/monads/curry.rb +2 -0
  12. data/lib/dry/monads/do.rb +55 -17
  13. data/lib/dry/monads/do/all.rb +39 -17
  14. data/lib/dry/monads/do/mixin.rb +2 -0
  15. data/lib/dry/monads/either.rb +9 -7
  16. data/lib/dry/monads/errors.rb +8 -3
  17. data/lib/dry/monads/lazy.rb +19 -6
  18. data/lib/dry/monads/list.rb +31 -30
  19. data/lib/dry/monads/maybe.rb +90 -19
  20. data/lib/dry/monads/registry.rb +15 -12
  21. data/lib/dry/monads/result.rb +42 -15
  22. data/lib/dry/monads/result/fixed.rb +35 -24
  23. data/lib/dry/monads/right_biased.rb +45 -24
  24. data/lib/dry/monads/task.rb +25 -22
  25. data/lib/dry/monads/transformer.rb +4 -1
  26. data/lib/dry/monads/traverse.rb +9 -1
  27. data/lib/dry/monads/try.rb +51 -13
  28. data/lib/dry/monads/unit.rb +6 -2
  29. data/lib/dry/monads/validated.rb +27 -20
  30. data/lib/dry/monads/version.rb +3 -1
  31. data/lib/json/add/dry/monads/maybe.rb +4 -3
  32. metadata +27 -75
  33. data/.codeclimate.yml +0 -12
  34. data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +0 -10
  35. data/.github/ISSUE_TEMPLATE/---bug-report.md +0 -34
  36. data/.github/ISSUE_TEMPLATE/---feature-request.md +0 -18
  37. data/.github/workflows/ci.yml +0 -74
  38. data/.github/workflows/docsite.yml +0 -34
  39. data/.github/workflows/sync_configs.yml +0 -34
  40. data/.gitignore +0 -10
  41. data/.rspec +0 -4
  42. data/.rubocop.yml +0 -89
  43. data/.yardopts +0 -4
  44. data/CODE_OF_CONDUCT.md +0 -13
  45. data/CONTRIBUTING.md +0 -29
  46. data/Gemfile +0 -23
  47. data/Rakefile +0 -6
  48. data/bin/console +0 -16
  49. data/bin/setup +0 -7
  50. data/docsite/source/case-equality.html.md +0 -42
  51. data/docsite/source/do-notation.html.md +0 -207
  52. data/docsite/source/getting-started.html.md +0 -142
  53. data/docsite/source/index.html.md +0 -179
  54. data/docsite/source/list.html.md +0 -87
  55. data/docsite/source/maybe.html.md +0 -146
  56. data/docsite/source/pattern-matching.html.md +0 -68
  57. data/docsite/source/result.html.md +0 -190
  58. data/docsite/source/task.html.md +0 -126
  59. data/docsite/source/tracing-failures.html.md +0 -32
  60. data/docsite/source/try.html.md +0 -76
  61. data/docsite/source/unit.html.md +0 -36
  62. data/docsite/source/validated.html.md +0 -88
  63. data/log/.gitkeep +0 -0
@@ -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
- def initialize(ctx)
6
- super("value! was called on #{ ctx.inspect }")
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 #{ failure.inspect }, it doesn't meet the constraints")
18
+ super("Cannot create Failure from #{failure.inspect}, it doesn't meet the constraints")
14
19
  end
15
20
  end
16
21
 
@@ -1,6 +1,9 @@
1
- require 'concurrent/promise'
1
+ # frozen_string_literal: true
2
2
 
3
- require 'dry/monads/task'
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
- "!#{ promise.reason.inspect }"
61
+ "!#{promise.reason.inspect}"
49
62
  else
50
- '?'
63
+ "?"
51
64
  end
52
65
 
53
- "Lazy(#{ state })"
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 'dry/monads/registry'
94
+ require "dry/monads/registry"
82
95
  register_mixin(:lazy, Lazy::Mixin)
83
96
  end
84
97
  end
@@ -1,12 +1,14 @@
1
- require 'dry/equalizer'
1
+ # frozen_string_literal: true
2
2
 
3
- require 'dry/monads/maybe'
4
- require 'dry/monads/task'
5
- require 'dry/monads/result'
6
- require 'dry/monads/try'
7
- require 'dry/monads/validated'
8
- require 'dry/monads/transformer'
9
- require 'dry/monads/curry'
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[:'dry-monads']
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
- # Note that this method returns an Array instance, not a List
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 block
143
+ if block_given?
142
144
  fmap(block)
143
145
  else
144
- value.map(&block)
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? ? "<#{ type.name.split('::').last }>" : ''
167
- "List#{ type_ann }#{ value.inspect }"
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) { |acc, v| yield(acc, v) }
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 { |e| yield(e) })
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) { yield }
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 [Result::Success, Result::Failure]
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[4, 2]
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 results
425
+ # List of maybes
424
426
  Maybe = ListBuilder[Maybe]
425
427
 
426
- # List of results
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 'dry/monads/registry'
452
+ require "dry/monads/registry"
452
453
  register_mixin(:list, List::Mixin)
453
454
  end
454
455
  end
455
456
 
456
- require 'dry/monads/traverse'
457
+ require "dry/monads/traverse"
@@ -1,10 +1,13 @@
1
- require 'dry/equalizer'
2
- require 'dry/core/deprecations'
1
+ # frozen_string_literal: true
3
2
 
4
- require 'dry/monads/right_biased'
5
- require 'dry/monads/transformer'
6
- require 'dry/monads/unit'
7
- require 'dry/monads/constants'
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[:'dry-monads']
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, 'nil cannot be some' if value.nil?
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
- alias_method :maybe, :fmap
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
- 'Some()'
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
- 'None'
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)) # => Some(foo: 1, bar: 2)
302
- # Maybe::Hash.filter(foo: Some(1), bar: None()) # => None()
303
- # Maybe::Hash.filter(foo: None(), bar: Some(2)) # => None()
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
- # @return [Maybe::Some]
394
+ extend Core::Deprecations[:"dry-monads"]
395
+
396
+ # @return [Maybe]
326
397
  def to_maybe
327
- Kernel.warn 'Success(nil) transformed to None' if @value.nil?
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 'dry/monads/registry'
460
+ require "dry/monads/registry"
390
461
  register_mixin(:maybe, Maybe::Mixin)
391
462
  end
392
463
  end
@@ -1,4 +1,6 @@
1
- require 'concurrent/map'
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: 'dry/monads/do/all',
12
- lazy: 'dry/monads/lazy',
13
- list: 'dry/monads/list',
14
- maybe: 'dry/monads/maybe',
15
- task: 'dry/monads/task',
16
- try: 'dry/monads/try',
17
- validated: 'dry/monads/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
- 'dry/monads/result',
20
- 'dry/monads/result/fixed'
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, "#{ name.inspect } is already registered"
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, "#{ name.inspect } is not a known monad"
55
+ raise ArgumentError, "#{name.inspect} is not a known monad"
53
56
  }
54
57
  Array(path).each { |p| require p }
55
58
  end