dry-monads 1.3.1 → 1.3.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.codeclimate.yml +10 -39
- data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +10 -0
- data/.github/ISSUE_TEMPLATE/---bug-report.md +34 -0
- data/.github/ISSUE_TEMPLATE/---feature-request.md +18 -0
- data/.github/workflows/ci.yml +74 -0
- data/.github/workflows/docsite.yml +34 -0
- data/.github/workflows/sync_configs.yml +34 -0
- data/.rspec +1 -0
- data/.rubocop.yml +89 -0
- data/CHANGELOG.md +21 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/CONTRIBUTING.md +4 -4
- data/Gemfile +4 -1
- data/LICENSE +17 -17
- data/README.md +2 -2
- data/docsite/source/case-equality.html.md +42 -0
- data/docsite/source/do-notation.html.md +207 -0
- data/docsite/source/getting-started.html.md +142 -0
- data/docsite/source/index.html.md +179 -0
- data/docsite/source/list.html.md +87 -0
- data/docsite/source/maybe.html.md +146 -0
- data/docsite/source/pattern-matching.html.md +68 -0
- data/docsite/source/result.html.md +190 -0
- data/docsite/source/task.html.md +126 -0
- data/docsite/source/tracing-failures.html.md +32 -0
- data/docsite/source/try.html.md +76 -0
- data/docsite/source/unit.html.md +36 -0
- data/docsite/source/validated.html.md +88 -0
- data/lib/dry/monads.rb +1 -0
- data/lib/dry/monads/{undefined.rb → constants.rb} +3 -1
- data/lib/dry/monads/do.rb +2 -1
- data/lib/dry/monads/do/all.rb +2 -2
- data/lib/dry/monads/do/mixin.rb +2 -3
- data/lib/dry/monads/maybe.rb +2 -4
- data/lib/dry/monads/result.rb +1 -1
- data/lib/dry/monads/right_biased.rb +52 -7
- data/lib/dry/monads/validated.rb +1 -1
- data/lib/dry/monads/version.rb +1 -1
- metadata +25 -5
- data/.travis.yml +0 -38
@@ -0,0 +1,190 @@
|
|
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
|
+
```
|
@@ -0,0 +1,126 @@
|
|
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
|
+
```
|
@@ -0,0 +1,32 @@
|
|
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.
|
@@ -0,0 +1,76 @@
|
|
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.
|
@@ -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
|
+
```
|