dry-monads 1.3.5 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +140 -80
  3. data/LICENSE +1 -1
  4. data/README.md +5 -4
  5. data/dry-monads.gemspec +30 -30
  6. data/lib/dry-monads.rb +1 -1
  7. data/lib/dry/monads.rb +2 -2
  8. data/lib/dry/monads/all.rb +2 -2
  9. data/lib/dry/monads/constants.rb +1 -1
  10. data/lib/dry/monads/do.rb +52 -18
  11. data/lib/dry/monads/do/all.rb +36 -17
  12. data/lib/dry/monads/either.rb +7 -7
  13. data/lib/dry/monads/errors.rb +5 -2
  14. data/lib/dry/monads/lazy.rb +15 -4
  15. data/lib/dry/monads/list.rb +28 -28
  16. data/lib/dry/monads/maybe.rb +87 -19
  17. data/lib/dry/monads/registry.rb +10 -10
  18. data/lib/dry/monads/result.rb +38 -12
  19. data/lib/dry/monads/result/fixed.rb +33 -24
  20. data/lib/dry/monads/right_biased.rb +35 -16
  21. data/lib/dry/monads/task.rb +20 -20
  22. data/lib/dry/monads/transformer.rb +2 -1
  23. data/lib/dry/monads/traverse.rb +7 -1
  24. data/lib/dry/monads/try.rb +45 -12
  25. data/lib/dry/monads/unit.rb +6 -2
  26. data/lib/dry/monads/validated.rb +20 -16
  27. data/lib/dry/monads/version.rb +1 -1
  28. data/lib/json/add/dry/monads/maybe.rb +3 -3
  29. metadata +18 -69
  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/log/.gitkeep +0 -0
  63. data/project.yml +0 -2
@@ -1,68 +0,0 @@
1
- ---
2
- title: Pattern matching
3
- layout: gem-single
4
- name: dry-monads
5
- ---
6
-
7
- Ruby 2.7 introduces pattern matching, it is nicely supported by dry-monads 1.3+.
8
-
9
- ### Matching Result values
10
-
11
- ```ruby
12
- # presumably you do it in a class with `include Dry::Monads[:result]`
13
-
14
- case value
15
- in Success(Integer => x)
16
- # x is bound to an integer
17
- in Success[:created, user] # alternatively: Success([:created, user])
18
- # user is bound to the second member
19
- in Success(Date | Time)
20
- # date or time object
21
- in Success([1, *])
22
- # any array starting with 1
23
- in Success(String => s) if s.size < 100
24
- # only if `s` is short enough
25
- in Success(counter: Integer)
26
- # matches Success(counter: 50)
27
- # doesn't match Success(counter: 50, extra: 50)
28
- in Success(user: User, account: Account => user_account)
29
- # matches Success(user: User.new(...), account: Account.new(...), else: ...)
30
- # user_account is bound to the value of the `:account` key
31
- in Success()
32
- # corresponds to Success(Unit)
33
- in Success(_)
34
- # general success
35
- in Failure[:user_not_found]
36
- # Failure([:user_not_found])
37
- in Failure[error_code, *payload]
38
- # ...
39
- end
40
- ```
41
-
42
- In the sippet above, the patterns will be tried sequentially. If `value` doesn't match any pattern, an error will be thrown.
43
-
44
- ### Matching Maybe
45
-
46
- ```ruby
47
- case value
48
- in Some(Integer => x) if x > 0
49
- # x is a positive integer
50
- in Some(Float | String)
51
- # ...
52
- in None
53
- # ...
54
- end
55
- ```
56
-
57
- ### Matching List
58
-
59
- ```ruby
60
- case value
61
- in List[Integer]
62
- # any list of size 1 with an integer
63
- in List[1, 2, 3, *]
64
- # list with size >= 3 starting with 1, 2, 3
65
- in List[]
66
- # empty list
67
- end
68
- ```
@@ -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.