dry-monads 1.3.0 → 1.3.5

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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +10 -39
  3. data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +10 -0
  4. data/.github/ISSUE_TEMPLATE/---bug-report.md +30 -0
  5. data/.github/ISSUE_TEMPLATE/---feature-request.md +18 -0
  6. data/.github/workflows/ci.yml +52 -0
  7. data/.github/workflows/docsite.yml +34 -0
  8. data/.github/workflows/sync_configs.yml +56 -0
  9. data/.rspec +1 -0
  10. data/.rubocop.yml +101 -0
  11. data/CHANGELOG.md +130 -62
  12. data/CODE_OF_CONDUCT.md +13 -0
  13. data/CONTRIBUTING.md +4 -4
  14. data/Gemfile +7 -8
  15. data/Gemfile.devtools +14 -0
  16. data/LICENSE +17 -17
  17. data/README.md +17 -38
  18. data/Rakefile +2 -0
  19. data/bin/.gitkeep +0 -0
  20. data/bin/console +1 -0
  21. data/docsite/source/case-equality.html.md +42 -0
  22. data/docsite/source/do-notation.html.md +207 -0
  23. data/docsite/source/getting-started.html.md +142 -0
  24. data/docsite/source/index.html.md +179 -0
  25. data/docsite/source/list.html.md +87 -0
  26. data/docsite/source/maybe.html.md +146 -0
  27. data/docsite/source/pattern-matching.html.md +68 -0
  28. data/docsite/source/result.html.md +190 -0
  29. data/docsite/source/task.html.md +126 -0
  30. data/docsite/source/tracing-failures.html.md +32 -0
  31. data/docsite/source/try.html.md +76 -0
  32. data/docsite/source/unit.html.md +36 -0
  33. data/docsite/source/validated.html.md +88 -0
  34. data/dry-monads.gemspec +7 -5
  35. data/lib/dry-monads.rb +2 -0
  36. data/lib/dry/monads.rb +3 -0
  37. data/lib/dry/monads/all.rb +2 -0
  38. data/lib/dry/monads/{undefined.rb → constants.rb} +3 -1
  39. data/lib/dry/monads/conversion_stubs.rb +2 -0
  40. data/lib/dry/monads/curry.rb +2 -0
  41. data/lib/dry/monads/do.rb +7 -2
  42. data/lib/dry/monads/do/all.rb +5 -2
  43. data/lib/dry/monads/do/mixin.rb +4 -3
  44. data/lib/dry/monads/either.rb +2 -0
  45. data/lib/dry/monads/errors.rb +4 -2
  46. data/lib/dry/monads/lazy.rb +4 -2
  47. data/lib/dry/monads/list.rb +10 -9
  48. data/lib/dry/monads/maybe.rb +9 -4
  49. data/lib/dry/monads/registry.rb +5 -2
  50. data/lib/dry/monads/result.rb +5 -4
  51. data/lib/dry/monads/result/fixed.rb +4 -2
  52. data/lib/dry/monads/right_biased.rb +55 -8
  53. data/lib/dry/monads/task.rb +6 -3
  54. data/lib/dry/monads/transformer.rb +2 -0
  55. data/lib/dry/monads/traverse.rb +2 -0
  56. data/lib/dry/monads/try.rb +6 -1
  57. data/lib/dry/monads/validated.rb +12 -9
  58. data/lib/dry/monads/version.rb +3 -1
  59. data/lib/json/add/dry/monads/maybe.rb +1 -0
  60. data/project.yml +2 -0
  61. metadata +45 -22
  62. data/.travis.yml +0 -41
@@ -0,0 +1,36 @@
1
+ ---
2
+ title: Unit
3
+ layout: gem-single
4
+ name: dry-monads
5
+ ---
6
+
7
+ Some constructors do not require you to pass a value. As a default they use `Unit`, a special singleton value:
8
+
9
+ ```ruby
10
+ extend Dry::Monads[:result]
11
+
12
+ Success().value! # => Unit
13
+ ```
14
+
15
+ `Unit` doesn't have any special properties or methods, it's similar to `nil` except for it is not i.e. `if Unit` passes.
16
+
17
+ `Unit` is usually excluded from the output:
18
+
19
+ ```ruby
20
+ extend Dry::Monads[:result]
21
+
22
+ # Outputs as "Success()" but technically it's "Success(Unit)"
23
+ Success()
24
+ ```
25
+
26
+ ### Discarding values
27
+
28
+ When the outcome of an operation is not a caller's concern, call `.discard`, it will map the wrapped value to `Unit`:
29
+
30
+ ```ruby
31
+ extend Dry::Monads[:result]
32
+
33
+ result = create_user # returns Success(#<User...>) or Failure(...)
34
+
35
+ result.discard # => Maps Success(#<User ...>) to Success() but lefts Failure(...) intact
36
+ ```
@@ -0,0 +1,88 @@
1
+ ---
2
+ title: Validated
3
+ layout: gem-single
4
+ name: dry-monads
5
+ ---
6
+
7
+ Suppose you've got a form to validate. If you are using `Result` combined with `Do` your code might look like this:
8
+
9
+ ```ruby
10
+ require 'dry/monads'
11
+
12
+ class CreateAccount
13
+ include Dry::Monads[:result, :do]
14
+
15
+ def call(form)
16
+ name = yield validate_name(form)
17
+ email = yield validate_email(form)
18
+ password = yield validate_password(form)
19
+
20
+ user = repo.create_user(
21
+ name: name,
22
+ email: email,
23
+ password: password
24
+ )
25
+
26
+ Success(user)
27
+ end
28
+
29
+ def validate_name(form)
30
+ # Success(name) or Failure(:invalid_name)
31
+ end
32
+
33
+ def validate_email(form)
34
+ # Success(email) or Failure(:invalid_email)
35
+ end
36
+
37
+ def validate_password(form)
38
+ # Success(password) or Failure(:invalid_password)
39
+ end
40
+ end
41
+ ```
42
+
43
+ If any of the validation steps fails the user will see an error. The problem is if `name` is not valid the user won't see errors about invalid `email` and `password`, if any. `Validated` circumvents this particular problem.
44
+
45
+ `Validated` is actually not a monad but an applicative functor. This means you can't call `bind` on it. Instead, it can accumulate values in combination with `List`:
46
+
47
+ ```ruby
48
+ require 'dry/monads'
49
+
50
+ class CreateAccount
51
+ include Dry::Monads[:list, :result, :validated, :do]
52
+
53
+ def call(form)
54
+ name, email, password = yield List::Validated[
55
+ validate_name(form),
56
+ validate_email(form),
57
+ validate_password(form)
58
+ ].traverse.to_result
59
+
60
+ user = repo.create_user(
61
+ name: name,
62
+ email: email,
63
+ password: password
64
+ )
65
+
66
+ Success(user)
67
+ end
68
+
69
+ def validate_name(form)
70
+ # Valid(name) or Invalid(:invalid_name)
71
+ end
72
+
73
+ def validate_email(form)
74
+ # Valid(email) or Invalid(:invalid_email)
75
+ end
76
+
77
+ def validate_password(form)
78
+ # Valid(password) or Invalid(:invalid_password)
79
+ end
80
+ end
81
+ ```
82
+
83
+ Here all validations will be processed at once, if any of them fails the result will be converted to a `Failure` wrapping the `List` of errors:
84
+
85
+ ```ruby
86
+ create_account.(form)
87
+ # => Failure(List[:invalid_name, :invalid_email])
88
+ ```
@@ -1,4 +1,6 @@
1
- lib = File.expand_path('../lib', __FILE__)
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
2
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
5
  require 'dry/monads/version'
4
6
 
@@ -25,13 +27,13 @@ Gem::Specification.new do |spec|
25
27
  spec.bindir = 'exe'
26
28
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
29
  spec.require_paths = ['lib']
28
- spec.required_ruby_version = ">= 2.4.0"
29
- spec.add_dependency 'dry-equalizer'
30
- spec.add_dependency 'dry-core', '~> 0.4', '>= 0.4.4'
30
+ spec.required_ruby_version = '>= 2.4.0'
31
31
  spec.add_dependency 'concurrent-ruby', '~> 1.0'
32
+ spec.add_dependency 'dry-core', '~> 0.4', '>= 0.4.4'
33
+ spec.add_dependency 'dry-equalizer'
32
34
 
33
35
  spec.add_development_dependency 'bundler'
36
+ spec.add_development_dependency 'dry-types', '>= 0.12'
34
37
  spec.add_development_dependency 'rake'
35
38
  spec.add_development_dependency 'rspec'
36
- spec.add_development_dependency 'dry-types', '>= 0.12'
37
39
  end
@@ -1 +1,3 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'dry/monads'
@@ -1,3 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/core/constants'
1
4
  require 'dry/monads/registry'
2
5
 
3
6
  module Dry
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'dry/monads'
2
4
  require 'dry/monads/registry'
3
5
 
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'dry/core/constants'
2
4
 
3
5
  module Dry
4
6
  module Monads
5
7
  # @private
6
- Undefined = Dry::Core::Constants::Undefined
8
+ include Core::Constants
7
9
  end
8
10
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Dry
2
4
  module Monads
3
5
  module ConversionStubs
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Dry
2
4
  module Monads
3
5
  # @private
@@ -1,5 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'dry/monads/list'
2
4
  require 'dry/monads/do/mixin'
5
+ require 'dry/monads/constants'
3
6
 
4
7
  module Dry
5
8
  module Monads
@@ -9,6 +12,8 @@ module Dry
9
12
  module Do
10
13
  extend Mixin
11
14
 
15
+ DELEGATE = ::RUBY_VERSION < '2.7' ? '*' : '...'
16
+
12
17
  # @api private
13
18
  class Halt < StandardError
14
19
  # @api private
@@ -102,7 +107,7 @@ module Dry
102
107
  # @api private
103
108
  def wrap_method(target, method_name)
104
109
  target.module_eval(<<-RUBY, __FILE__, __LINE__ + 1)
105
- def #{method_name}(*)
110
+ def #{method_name}(#{DELEGATE})
106
111
  if block_given?
107
112
  super
108
113
  else
@@ -130,7 +135,7 @@ module Dry
130
135
 
131
136
  # @api private
132
137
  def halt(result)
133
- raise Halt.new(result)
138
+ raise Halt.new(result), EMPTY_STRING, []
134
139
  end
135
140
  end
136
141
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'dry/monads/do'
2
4
 
3
5
  module Dry
@@ -56,7 +58,7 @@ module Dry
56
58
  #
57
59
  module All
58
60
  # @private
59
- class MethodTracker < Module
61
+ class MethodTracker < ::Module
60
62
  attr_reader :wrappers
61
63
 
62
64
  def initialize(wrappers)
@@ -80,7 +82,7 @@ module Dry
80
82
 
81
83
  def included(base)
82
84
  super
83
- Dry::Monads::Do::All.included(base)
85
+ All.included(base)
84
86
  end
85
87
  end
86
88
  end
@@ -119,6 +121,7 @@ module Dry
119
121
  super(method)
120
122
 
121
123
  next if method.equal?(:singleton_method_added)
124
+
122
125
  Do.wrap_method(wrapper, method)
123
126
  end
124
127
  object.singleton_class.instance_methods(false).each do |m|
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Dry
2
4
  module Monads
3
5
  module Do
@@ -43,14 +45,13 @@ module Dry
43
45
  # @api public
44
46
  def bind(monads)
45
47
  monads = Do.coerce_to_monad(Array(monads))
46
- unwrapped = monads.map { |result|
48
+ unwrapped = monads.map do |result|
47
49
  monad = result.to_monad
48
50
  monad.or { Do.halt(monad) }.value!
49
- }
51
+ end
50
52
  monads.size == 1 ? unwrapped[0] : unwrapped
51
53
  end
52
54
  end
53
55
  end
54
56
  end
55
57
  end
56
-
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'dry/core/deprecations'
2
4
 
3
5
  Dry::Core::Deprecations.warn('Either monad was renamed to Result', tag: :'dry-monads')
@@ -1,16 +1,18 @@
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
7
  def initialize(ctx)
6
- super("value! was called on #{ ctx.inspect }")
8
+ super("value! was called on #{ctx.inspect}")
7
9
  end
8
10
  end
9
11
 
10
12
  # An error thrown on returning a Failure of unknown type.
11
13
  class InvalidFailureTypeError < StandardError
12
14
  def initialize(failure)
13
- super("Cannot create Failure from #{ failure.inspect }, it doesn't meet the constraints")
15
+ super("Cannot create Failure from #{failure.inspect}, it doesn't meet the constraints")
14
16
  end
15
17
  end
16
18
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'concurrent/promise'
2
4
 
3
5
  require 'dry/monads/task'
@@ -45,12 +47,12 @@ module Dry
45
47
  when :fulfilled
46
48
  value!.inspect
47
49
  when :rejected
48
- "!#{ promise.reason.inspect }"
50
+ "!#{promise.reason.inspect}"
49
51
  else
50
52
  '?'
51
53
  end
52
54
 
53
- "Lazy(#{ state })"
55
+ "Lazy(#{state})"
54
56
  end
55
57
  alias_method :inspect, :to_s
56
58
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'dry/equalizer'
2
4
 
3
5
  require 'dry/monads/maybe'
@@ -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
 
@@ -263,10 +265,10 @@ module Dry
263
265
  def typed(type = nil)
264
266
  if type.nil?
265
267
  if size.zero?
266
- raise ArgumentError, "Cannot infer a monad for an empty list"
268
+ raise ArgumentError, 'Cannot infer a monad for an empty list'
267
269
  else
268
270
  self.class.warn(
269
- "Automatic monad inference is deprecated, pass a type explicitly "\
271
+ 'Automatic monad inference is deprecated, pass a type explicitly '\
270
272
  "or use a predefined constant, e.g. List::Result\n"\
271
273
  "#{caller.find { |l| l !~ %r{(lib/dry/monads)|(gems)} }}"
272
274
  )
@@ -296,16 +298,16 @@ module Dry
296
298
  # @return [Monad] Result is a monadic value
297
299
  def traverse(proc = nil, &block)
298
300
  unless typed?
299
- raise StandardError, "Cannot traverse an untyped list"
301
+ raise StandardError, 'Cannot traverse an untyped list'
300
302
  end
301
303
 
302
304
  cons = type.pure { |list, i| list + List.pure(i) }
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
 
@@ -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
 
@@ -1,11 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'dry/equalizer'
2
- require 'dry/core/constants'
3
4
  require 'dry/core/deprecations'
4
5
 
5
6
  require 'dry/monads/right_biased'
6
7
  require 'dry/monads/transformer'
7
8
  require 'dry/monads/unit'
8
- require 'dry/monads/undefined'
9
+ require 'dry/monads/constants'
9
10
 
10
11
  module Dry
11
12
  module Monads
@@ -16,7 +17,7 @@ module Dry
16
17
  include Transformer
17
18
 
18
19
  class << self
19
- extend Dry::Core::Deprecations[:'dry-monads']
20
+ extend Core::Deprecations[:'dry-monads']
20
21
 
21
22
  # Wraps the given value with into a Maybe object.
22
23
  #
@@ -105,6 +106,7 @@ module Dry
105
106
 
106
107
  def initialize(value = Undefined)
107
108
  raise ArgumentError, 'nil cannot be some' if value.nil?
109
+
108
110
  @value = Undefined.default(value, Unit)
109
111
  end
110
112
 
@@ -139,7 +141,6 @@ module Dry
139
141
  # @api public
140
142
  class None < Maybe
141
143
  include RightBiased::Left
142
- include Core::Constants
143
144
 
144
145
  @instance = new.freeze
145
146
  singleton_class.send(:attr_reader, :instance)
@@ -163,6 +164,10 @@ module Dry
163
164
  @trace = trace
164
165
  end
165
166
 
167
+ # @!method maybe(*args, &block)
168
+ # Alias of fmap, returns self back
169
+ alias_method :maybe, :fmap
170
+
166
171
  # If a block is given passes internal value to it and returns the result,
167
172
  # otherwise simply returns the parameter val.
168
173
  #
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'concurrent/map'
2
4
 
3
5
  module Dry
@@ -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