dry-monads 1.3.0 → 1.3.5

Sign up to get free protection for your applications and to get access to all the features.
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