dry-monads 1.3.5 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +161 -80
  3. data/LICENSE +1 -1
  4. data/README.md +5 -4
  5. data/dry-monads.gemspec +30 -30
  6. data/lib/dry/monads/all.rb +2 -3
  7. data/lib/dry/monads/constants.rb +0 -2
  8. data/lib/dry/monads/curry.rb +2 -2
  9. data/lib/dry/monads/do/all.rb +35 -18
  10. data/lib/dry/monads/do.rb +48 -20
  11. data/lib/dry/monads/errors.rb +8 -5
  12. data/lib/dry/monads/lazy.rb +13 -5
  13. data/lib/dry/monads/list.rb +27 -37
  14. data/lib/dry/monads/maybe.rb +85 -26
  15. data/lib/dry/monads/registry.rb +20 -20
  16. data/lib/dry/monads/result/fixed.rb +31 -24
  17. data/lib/dry/monads/result.rb +37 -19
  18. data/lib/dry/monads/right_biased.rb +38 -31
  19. data/lib/dry/monads/task.rb +25 -28
  20. data/lib/dry/monads/transformer.rb +2 -1
  21. data/lib/dry/monads/traverse.rb +5 -1
  22. data/lib/dry/monads/try.rb +45 -18
  23. data/lib/dry/monads/unit.rb +9 -3
  24. data/lib/dry/monads/validated.rb +18 -18
  25. data/lib/dry/monads/version.rb +1 -1
  26. data/lib/dry/monads.rb +25 -4
  27. data/lib/dry-monads.rb +1 -1
  28. data/lib/json/add/dry/monads/maybe.rb +5 -5
  29. metadata +22 -68
  30. data/.codeclimate.yml +0 -12
  31. data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +0 -10
  32. data/.github/ISSUE_TEMPLATE/---bug-report.md +0 -30
  33. data/.github/ISSUE_TEMPLATE/---feature-request.md +0 -18
  34. data/.github/workflows/ci.yml +0 -52
  35. data/.github/workflows/docsite.yml +0 -34
  36. data/.github/workflows/sync_configs.yml +0 -56
  37. data/.gitignore +0 -10
  38. data/.rspec +0 -4
  39. data/.rubocop.yml +0 -101
  40. data/.yardopts +0 -4
  41. data/CODE_OF_CONDUCT.md +0 -13
  42. data/CONTRIBUTING.md +0 -29
  43. data/Gemfile +0 -19
  44. data/Gemfile.devtools +0 -14
  45. data/Rakefile +0 -8
  46. data/bin/.gitkeep +0 -0
  47. data/bin/console +0 -17
  48. data/bin/setup +0 -7
  49. data/docsite/source/case-equality.html.md +0 -42
  50. data/docsite/source/do-notation.html.md +0 -207
  51. data/docsite/source/getting-started.html.md +0 -142
  52. data/docsite/source/index.html.md +0 -179
  53. data/docsite/source/list.html.md +0 -87
  54. data/docsite/source/maybe.html.md +0 -146
  55. data/docsite/source/pattern-matching.html.md +0 -68
  56. data/docsite/source/result.html.md +0 -190
  57. data/docsite/source/task.html.md +0 -126
  58. data/docsite/source/tracing-failures.html.md +0 -32
  59. data/docsite/source/try.html.md +0 -76
  60. data/docsite/source/unit.html.md +0 -36
  61. data/docsite/source/validated.html.md +0 -88
  62. data/lib/dry/monads/either.rb +0 -66
  63. data/log/.gitkeep +0 -0
  64. data/project.yml +0 -2
@@ -1,190 +0,0 @@
1
- ---
2
- title: Result
3
- layout: gem-single
4
- name: dry-monads
5
- ---
6
-
7
- The `Result` monad is useful to express a series of computations that might
8
- return an error object with additional information.
9
-
10
- The `Result` mixin has two type constructors: `Success` and `Failure`. The `Success`
11
- can be thought of as "everything went success" and the `Failure` is used when
12
- "something has gone wrong".
13
-
14
- ### `bind`
15
-
16
- Use `bind` for composing several possibly-failing operations:
17
-
18
- ```ruby
19
- require 'dry/monads'
20
-
21
- class AssociateUser
22
- include Dry::Monads[:result]
23
-
24
- def call(user_id:, address_id:)
25
- find_user(user_id).bind do |user|
26
- find_address(address_id).fmap do |address|
27
- user.update(address_id: address.id)
28
- end
29
- end
30
- end
31
-
32
- private
33
-
34
- def find_user(id)
35
- user = User.find_by(id: id)
36
-
37
- if user
38
- Success(user)
39
- else
40
- Failure(:user_not_found)
41
- end
42
- end
43
-
44
- def find_address(id)
45
- address = Address.find_by(id: id)
46
-
47
- if address
48
- Success(address)
49
- else
50
- Failure(:address_not_found)
51
- end
52
- end
53
- end
54
-
55
- AssociateUser.new.(user_id: 1, address_id: 2)
56
- ```
57
-
58
- ### `fmap`
59
-
60
- An example of using `fmap` with `Success` and `Failure`.
61
-
62
- ```ruby
63
- extend Dry::Monads[:result]
64
-
65
- result = if foo > bar
66
- Success(10)
67
- else
68
- Failure("wrong")
69
- end.fmap { |x| x * 2 }
70
-
71
- # If everything went success
72
- result # => Success(20)
73
- # If it did not
74
- result # => Failure("wrong")
75
-
76
- # #fmap accepts a proc, just like #bind
77
-
78
- upcase = :upcase.to_proc
79
-
80
- Success('hello').fmap(upcase) # => Success("HELLO")
81
- ```
82
-
83
- ### `value_or`
84
-
85
- `value_or` is a safe and recommended way of extracting values.
86
-
87
- ```ruby
88
- extend Dry::Monads[:result]
89
-
90
- Success(10).value_or(0) # => 10
91
- Failure('Error').value_or(0) # => 0
92
- ```
93
-
94
- ### `value!`
95
-
96
- If you're 100% sure you're dealing with a `Success` case you might use `value!` for extracting the value without providing a default. Beware, this will raise an exception if you call it on `Failure`.
97
-
98
- ```ruby
99
- extend Dry::Monads[:result]
100
-
101
- Success(10).value! # => 10
102
- Failure('Error').value!
103
- # => Dry::Monads::UnwrapError: value! was called on Failure
104
- ```
105
-
106
- ### `or`
107
-
108
- An example of using `or` with `Success` and `Failure`.
109
-
110
- ```ruby
111
- extend Dry::Monads[:result]
112
-
113
- Success(10).or(Success(99)) # => Success(10)
114
- Failure("error").or(Failure("new error")) # => Failure("new error")
115
- Failure("error").or { |err| Failure("new #{err}") } # => Failure("new error")
116
- ```
117
-
118
- ### `failure`
119
-
120
- Use `failure` for unwrapping the value from a `Failure` instance.
121
-
122
- ```ruby
123
- extend Dry::Monads[:result]
124
-
125
- Failure('Error').failure # => "Error"
126
- ```
127
-
128
- ### `to_maybe`
129
-
130
- Sometimes it's useful to turn a `Result` into a `Maybe`.
131
-
132
- ```ruby
133
- extend Dry::Monads[:result, :maybe]
134
-
135
- result = if foo > bar
136
- Success(10)
137
- else
138
- Failure("wrong")
139
- end.to_maybe
140
-
141
- # If everything went success
142
- result # => Some(10)
143
- # If it did not
144
- result # => None()
145
- ```
146
-
147
- ### `failure?` and `success?`
148
-
149
- You can explicitly check the type by calling `failure?` or `success?` on a monadic value.
150
-
151
- ### `either`
152
-
153
- `either` maps a `Result` to some type by taking two callables, for `Success` and `Failure` cases respectively:
154
-
155
- ```ruby
156
- Success(1).either(-> x { x + 1 }, -> x { x + 2 }) # => 2
157
- Failure(1).either(-> x { x + 1 }, -> x { x + 2 }) # => 3
158
- ```
159
-
160
-
161
- ### Adding constraints to `Failure` values.
162
- You can add type constraints to values passed to `Failure`. This will raise an exception if value doesn't meet the constraints:
163
-
164
- ```ruby
165
- require 'dry-types'
166
-
167
- module Types
168
- include Dry.Types()
169
- end
170
-
171
- class Operation
172
- Error = Types.Instance(RangeError)
173
- include Dry::Monads::Result(Error)
174
-
175
- def call(value)
176
- case value
177
- when 0..1
178
- Success(:success)
179
- when -Float::INFINITY..0, 1..Float::INFINITY
180
- Failure(RangeError.new('Error'))
181
- else
182
- Failure(TypeError.new('Type error'))
183
- end
184
- end
185
- end
186
-
187
- Operation.new.call(0.5) # => Success(:success)
188
- Operation.new.call(5) # => Failure(#<RangeError: Error>)
189
- Operation.new.call("5") # => Dry::Monads::InvalidFailureTypeError: Cannot create Failure from #<TypeError: Type error>, it doesn't meet the constraints
190
- ```
@@ -1,126 +0,0 @@
1
- ---
2
- title: Task
3
- layout: gem-single
4
- name: dry-monads
5
- ---
6
-
7
- `Task` represents an asynchronous computation. It is similar to the `IO` type in a sense it can be used to wrap side-effectful actions. `Task`s are usually run on a thread pool but also can be executed immediately on the current thread. Internally, `Task` uses `Promise` from the [`concurrent-ruby`](https://github.com/ruby-concurrency/concurrent-ruby) gem, basically it's a thin wrapper with a monadic interface which makes it easily composable with other monads.
8
-
9
- ### `Task::Mixin`
10
-
11
- Basic usage.
12
-
13
- ```ruby
14
- require 'dry/monads'
15
-
16
- class PullUsersWithPosts
17
- include Dry::Monads[:task]
18
-
19
- def call
20
- # Start two tasks running concurrently
21
- users = Task { fetch_users }
22
- posts = Task { fetch_posts }
23
-
24
- # Combine two tasks
25
- users.bind { |us| posts.fmap { |ps| [us, ps] } }
26
- end
27
-
28
- def fetch_users
29
- sleep 3
30
- [{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }]
31
- end
32
-
33
- def fetch_posts
34
- sleep 2
35
- [
36
- { id: 1, user_id: 1, name: 'Hello from John' },
37
- { id: 2, user_id: 2, name: 'Hello from Jane' },
38
- ]
39
- end
40
- end
41
-
42
- # PullUsersWithPosts instance
43
- pull = PullUsersWithPosts.new
44
-
45
- # Spin up two tasks
46
- task = pull.call
47
-
48
- task.fmap do |users, posts|
49
- puts "Users: #{ users.inspect }"
50
- puts "Posts: #{ posts.inspect }"
51
- end
52
-
53
- puts "----" # this will be printed before the lines above
54
- ```
55
-
56
- ### Executors
57
-
58
- Tasks are performed by executors, there are three executors predefined by `concurrent-ruby` identified by symbols:
59
-
60
- - `:fast` – for fast asynchronous tasks, uses a thread pool
61
- - `:io` – for long IO-bound tasks, uses a thread pool, different from `:fast`
62
- - `:immediate` – runs tasks immediately, on the current thread. Can be used in tests or for other purposes
63
-
64
- You can create your own executors, check out the [docs](http://ruby-concurrency.github.io/concurrent-ruby/master/Concurrent.html) for more on this.
65
-
66
- The following examples use the Ruby 2.5+ syntax which allows passing a block to `.[]`.
67
-
68
- ```ruby
69
- Task[:io] { do_http_request }
70
-
71
- Task[:fast] { cpu_intensive_computation }
72
-
73
- Task[:immediate] { unsafe_io_operation }
74
-
75
- # You can pass an executor object
76
- Task[my_executor] { ... }
77
- ```
78
-
79
- ### Exception handling
80
-
81
- All exceptions happening in `Task` are captured, even if you're using the `:immediate` executor, they won't be re-raised.
82
-
83
- ```ruby
84
- io_fail = Task[:io] { 1/0 }
85
- io_fail # => Task(error=#<ZeroDivisionError: divided by 0>)
86
-
87
- immediate_fail = Task[:immediate] { 1/0 }
88
- immediate_fail # => Task(error=#<ZeroDivisionError: divided by 0>)
89
- ```
90
-
91
- You can process failures with `or` and `or_fmap`:
92
-
93
- ```ruby
94
- Task[:immediate] { 1/0 }.or { M::Task[:immediate] { 0 } } # => Task(value=0)
95
- Task[:immediate] { 1/0 }.or_fmap { 0 } # => Task(value=0)
96
- ```
97
-
98
- ### Extracting result
99
-
100
- Getting the result of a task is an unsafe operation, it blocks the current thread until the task is finished, then returns the value or raises an exception if the evaluation wasn't sucessful. It effectively cancels all niceties of tasks so you shouldn't use it in production code.
101
-
102
- ```ruby
103
- Task { 0 }.value! # => 0
104
- Task { 1/0 }.value! # => ZeroDivisionError: divided by 0
105
- ```
106
-
107
- You can wait for a task to complete, the `wait` method accepts an optional timeout. `.wait` returns the task back, without unwrapping the result so it's a blocking yet safe operation:
108
-
109
- ```ruby
110
- Task[:io] { 2 }.wait(1) # => Task(value=2)
111
- Task[:io] { sleep 2; 2 }.wait(1) # => Task(?)
112
-
113
- # (?) denotes an unfinished computation
114
- ```
115
-
116
- ### Conversions
117
-
118
- Tasks can be converted to other monads but keep in mind that all conversions block the current thread:
119
-
120
- ```ruby
121
- Task[:io] { 2 }.to_result # => Success(2)
122
- Task[:io] { 1/0 }.to_result # => Failure(#<ZeroDivisionError: divided by 0>)
123
-
124
- Task[:io] { 2 }.to_maybe # => Some(2)
125
- Task[:io] { 1/0 }.to_maybe # => None
126
- ```
@@ -1,32 +0,0 @@
1
- ---
2
- title: Tracing failures
3
- layout: gem-single
4
- name: dry-monads
5
- ---
6
-
7
- "Left" values of right-biased monads like `Maybe` and `Result` (as in, `None()` and `Failure()`) ignore blocks passed to `fmap` and `bind`. Because of this, these values travel across the application without any modification. If the place where a `Failure` was constructed is burried somewhere deep in the app or library code it may be pretty hard to find out where exactly the error occurred.
8
-
9
- This is a noticable downside compared to "good" old exceptions. To address it, every `Failure(...)` and `None()` value tracks the line where it was created:
10
-
11
- ```ruby
12
- # create_user.rb
13
- require 'dry/monads'
14
-
15
- class CreateUser
16
- include Dry::Monads[:result]
17
-
18
- def call
19
- Failure(:no_luck)
20
- end
21
- end
22
- ```
23
-
24
- ```ruby
25
- require 'create_user'
26
-
27
- create_user = CreateUser.new
28
- create_user.() # => Failure(:no_luck)
29
- create_user.().trace # => .../create_user.rb:8:in `call'
30
- ```
31
-
32
- Note that the trace stores only one line of the stack so it shouldn't ever be a performance issue.
@@ -1,76 +0,0 @@
1
- ---
2
- title: Try
3
- layout: gem-single
4
- name: dry-monads
5
- ---
6
-
7
- Rescues a block from an exception. The `Try` monad is useful when you want to wrap some code that can raise exceptions of certain types. A common example is making an HTTP request or querying a database.
8
-
9
- ```ruby
10
- require 'dry/monads'
11
-
12
- class ExceptionalLand
13
- include Dry::Monads[:try]
14
-
15
-
16
- def call
17
- res = Try { 10 / 2 }
18
- res.value! if res.value?
19
- # => 5
20
-
21
- res = Try { 10 / 0 }
22
- res.exception if res.error?
23
- # => #<ZeroDivisionError: divided by 0>
24
-
25
- # By default Try catches all exceptions inherited from StandardError.
26
- # However you can catch only certain exceptions like this
27
- Try[NoMethodError, NotImplementedError] { 10 / 0 }
28
- # => raised ZeroDivisionError: divided by 0 exception
29
- end
30
- end
31
- ```
32
-
33
- It is better if you pass a list of expected exceptions which you are sure you can process. Catching exceptions of all types is considered bad practice.
34
-
35
- The `Try` monad consists of two types: `Value` and `Error`. The first is returned when code did not raise an error and the second is returned when the error was captured.
36
-
37
- ### `bind`
38
-
39
- Allows you to chain blocks that can raise exceptions.
40
-
41
- ```ruby
42
- Try[NetworkError, DBError] { grap_user_by_making_request }.bind { |user| user_repo.save(user) }
43
-
44
- # Possible outcomes:
45
- # => Value(persisted_user)
46
- # => Error(NetworkError: request timeout)
47
- # => Error(DBError: unique constraint violated)
48
- ```
49
-
50
- ### `fmap`
51
-
52
- Works exactly the same way as `Result#fmap` does.
53
-
54
- ```ruby
55
- require 'dry/monads'
56
-
57
- class ExceptionalLand
58
- include Dry::Monads[:try]
59
-
60
- def call
61
- Try { 10 / 2 }.fmap { |x| x * 3 }
62
- # => 15
63
-
64
- Try[ZeroDivisionError] { 10 / 0 }.fmap { |x| x * 3 }
65
- # => Failure(ZeroDivisionError: divided by 0)
66
- end
67
- end
68
- ```
69
-
70
- ### `value!` and `exception`
71
-
72
- Use `value!` for unwrapping a `Success` and `exception` for getting error object from a `Failure`.
73
-
74
- ### `to_result` and `to_maybe`
75
-
76
- `Try`'s `Value` and `Error` can be transformed to `Success` and `Failure` correspondingly by calling `to_result` and to `Some` and `None` by calling `to_maybe`. Keep in mind that by transforming `Try` to `Maybe` you lose the information about an exception so be sure that you've processed the error before doing so.
@@ -1,36 +0,0 @@
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
- ```
@@ -1,88 +0,0 @@
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,66 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'dry/core/deprecations'
4
-
5
- Dry::Core::Deprecations.warn('Either monad was renamed to Result', tag: :'dry-monads')
6
-
7
- require 'dry/monads/result'
8
-
9
- module Dry
10
- module Monads
11
- Either = Result
12
- deprecate_constant :Either
13
-
14
- class Result
15
- extend Dry::Core::Deprecations[:'dry-monads']
16
-
17
- deprecate :to_either, :to_result
18
-
19
- Right = Success
20
- Left = Failure
21
-
22
- deprecate_constant :Right
23
- deprecate_constant :Left
24
-
25
- module Mixin
26
- module Constructors
27
- extend Dry::Core::Deprecations[:'dry-monads']
28
-
29
- Right = Success
30
- Left = Failure
31
- deprecate_constant :Right
32
- deprecate_constant :Left
33
-
34
- deprecate :Right, :Success
35
- deprecate :Left, :Failure
36
- end
37
- end
38
-
39
- class Success
40
- deprecate :left?, :failure?
41
- deprecate :right?, :success?
42
- end
43
-
44
- class Failure
45
- deprecate :left?, :failure?
46
- deprecate :right?, :success?
47
-
48
- deprecate :left, :failure
49
- end
50
- end
51
-
52
- class Try
53
- class Value
54
- extend Dry::Core::Deprecations[:'dry-monads']
55
-
56
- deprecate :to_either, :to_result
57
- end
58
-
59
- class Error
60
- extend Dry::Core::Deprecations[:'dry-monads']
61
-
62
- deprecate :to_either, :to_result
63
- end
64
- end
65
- end
66
- end
data/log/.gitkeep DELETED
File without changes
data/project.yml DELETED
@@ -1,2 +0,0 @@
1
- name: dry-monads
2
- codacy_id: f2eed41bf7f04b38b0a7691c2cf6e73c