dry-monads 1.3.2 → 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +157 -73
- data/LICENSE +1 -1
- data/README.md +18 -38
- data/dry-monads.gemspec +32 -30
- data/lib/dry-monads.rb +3 -1
- data/lib/dry/monads.rb +4 -2
- data/lib/dry/monads/all.rb +4 -2
- data/lib/dry/monads/constants.rb +1 -1
- data/lib/dry/monads/conversion_stubs.rb +2 -0
- data/lib/dry/monads/curry.rb +2 -0
- data/lib/dry/monads/do.rb +55 -17
- data/lib/dry/monads/do/all.rb +39 -17
- data/lib/dry/monads/do/mixin.rb +2 -0
- data/lib/dry/monads/either.rb +9 -7
- data/lib/dry/monads/errors.rb +8 -3
- data/lib/dry/monads/lazy.rb +19 -6
- data/lib/dry/monads/list.rb +31 -30
- data/lib/dry/monads/maybe.rb +90 -19
- data/lib/dry/monads/registry.rb +15 -12
- data/lib/dry/monads/result.rb +42 -15
- data/lib/dry/monads/result/fixed.rb +35 -24
- data/lib/dry/monads/right_biased.rb +45 -24
- data/lib/dry/monads/task.rb +25 -22
- data/lib/dry/monads/transformer.rb +4 -1
- data/lib/dry/monads/traverse.rb +9 -1
- data/lib/dry/monads/try.rb +51 -13
- data/lib/dry/monads/unit.rb +6 -2
- data/lib/dry/monads/validated.rb +27 -20
- data/lib/dry/monads/version.rb +3 -1
- data/lib/json/add/dry/monads/maybe.rb +4 -3
- metadata +27 -75
- data/.codeclimate.yml +0 -12
- data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +0 -10
- data/.github/ISSUE_TEMPLATE/---bug-report.md +0 -34
- data/.github/ISSUE_TEMPLATE/---feature-request.md +0 -18
- data/.github/workflows/ci.yml +0 -74
- data/.github/workflows/docsite.yml +0 -34
- data/.github/workflows/sync_configs.yml +0 -34
- data/.gitignore +0 -10
- data/.rspec +0 -4
- data/.rubocop.yml +0 -89
- data/.yardopts +0 -4
- data/CODE_OF_CONDUCT.md +0 -13
- data/CONTRIBUTING.md +0 -29
- data/Gemfile +0 -23
- data/Rakefile +0 -6
- data/bin/console +0 -16
- data/bin/setup +0 -7
- data/docsite/source/case-equality.html.md +0 -42
- data/docsite/source/do-notation.html.md +0 -207
- data/docsite/source/getting-started.html.md +0 -142
- data/docsite/source/index.html.md +0 -179
- data/docsite/source/list.html.md +0 -87
- data/docsite/source/maybe.html.md +0 -146
- data/docsite/source/pattern-matching.html.md +0 -68
- data/docsite/source/result.html.md +0 -190
- data/docsite/source/task.html.md +0 -126
- data/docsite/source/tracing-failures.html.md +0 -32
- data/docsite/source/try.html.md +0 -76
- data/docsite/source/unit.html.md +0 -36
- data/docsite/source/validated.html.md +0 -88
- data/log/.gitkeep +0 -0
data/docsite/source/list.html.md
DELETED
@@ -1,87 +0,0 @@
|
|
1
|
-
---
|
2
|
-
title: List
|
3
|
-
layout: gem-single
|
4
|
-
name: dry-monads
|
5
|
-
---
|
6
|
-
|
7
|
-
### `bind`
|
8
|
-
|
9
|
-
Lifts a block/proc and runs it against each member of the list. The block must return a value coercible to a list. As in other monads if no block given the first argument will be treated as callable and used instead.
|
10
|
-
|
11
|
-
```ruby
|
12
|
-
require 'dry/monads/list'
|
13
|
-
|
14
|
-
M = Dry::Monads
|
15
|
-
|
16
|
-
M::List[1, 2].bind { |x| [x + 1] } # => List[2, 3]
|
17
|
-
M::List[1, 2].bind(-> x { [x, x + 1] }) # => List[1, 2, 2, 3]
|
18
|
-
|
19
|
-
M::List[1, nil].bind { |x| [x + 1] } # => error
|
20
|
-
```
|
21
|
-
|
22
|
-
### `fmap`
|
23
|
-
|
24
|
-
Maps a block over the list. Acts as `Array#map`. As in other monads, if no block given the first argument will be treated as callable and used instead.
|
25
|
-
|
26
|
-
```ruby
|
27
|
-
require 'dry/monads/list'
|
28
|
-
|
29
|
-
M = Dry::Monads
|
30
|
-
|
31
|
-
M::List[1, 2].fmap { |x| x + 1 } # => List[2, 3]
|
32
|
-
```
|
33
|
-
|
34
|
-
### `value`
|
35
|
-
|
36
|
-
You always can unwrap the result by calling `value`.
|
37
|
-
|
38
|
-
```ruby
|
39
|
-
require 'dry/monads/list'
|
40
|
-
|
41
|
-
M = Dry::Monads
|
42
|
-
|
43
|
-
M::List[1, 2].value # => [1, 2]
|
44
|
-
```
|
45
|
-
|
46
|
-
### Concatenation
|
47
|
-
|
48
|
-
```ruby
|
49
|
-
require 'dry/monads/list'
|
50
|
-
|
51
|
-
M = Dry::Monads
|
52
|
-
|
53
|
-
M::List[1, 2] + M::List[3, 4] # => List[1, 2, 3, 4]
|
54
|
-
```
|
55
|
-
|
56
|
-
### `head` and `tail`
|
57
|
-
|
58
|
-
`head` returns the first element wrapped with a `Maybe`.
|
59
|
-
|
60
|
-
```ruby
|
61
|
-
require 'dry/monads/list'
|
62
|
-
|
63
|
-
M = Dry::Monads
|
64
|
-
|
65
|
-
M::List[1, 2, 3, 4].head # => Some(1)
|
66
|
-
M::List[1, 2, 3, 4].tail # => List[2, 3, 4]
|
67
|
-
```
|
68
|
-
|
69
|
-
### `traverse`
|
70
|
-
|
71
|
-
Traverses the list with a block (or without it). This method "flips" List structure with the given monad (obtained from the type).
|
72
|
-
|
73
|
-
**Note that traversing requires the list to be typed.**
|
74
|
-
|
75
|
-
```ruby
|
76
|
-
require 'dry/monads/list'
|
77
|
-
|
78
|
-
M = Dry::Monads
|
79
|
-
|
80
|
-
M::List[M::Success(1), M::Success(2)].typed(M::Result).traverse # => Success([1, 2])
|
81
|
-
M::List[M::Maybe(1), M::Maybe(nil), M::Maybe(3)].typed(M::Maybe).traverse # => None
|
82
|
-
|
83
|
-
# also, you can use fmap with #traverse
|
84
|
-
|
85
|
-
M::List[1, 2].fmap { |x| M::Success(x) }.typed(M::Result).traverse # => Success([1, 2])
|
86
|
-
M::List[1, nil, 3].fmap { |x| M::Maybe(x) }.typed(M::Maybe).traverse # => None
|
87
|
-
```
|
@@ -1,146 +0,0 @@
|
|
1
|
-
---
|
2
|
-
title: Maybe
|
3
|
-
layout: gem-single
|
4
|
-
name: dry-monads
|
5
|
-
---
|
6
|
-
|
7
|
-
The `Maybe` monad is used when a series of computations could return `nil` at any point.
|
8
|
-
|
9
|
-
### `bind`
|
10
|
-
|
11
|
-
Applies a block to a monadic value. If the value is `Some` then calls the block passing the unwrapped value as an argument. Returns itself if the value is `None`.
|
12
|
-
|
13
|
-
```ruby
|
14
|
-
extend Dry::Monads[:maybe]
|
15
|
-
|
16
|
-
maybe_user = Maybe(user).bind do |u|
|
17
|
-
Maybe(u.address).bind do |a|
|
18
|
-
Maybe(a.street)
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
# If user with address exists
|
23
|
-
# => Some("Street Address")
|
24
|
-
# If user or address is nil
|
25
|
-
# => None()
|
26
|
-
|
27
|
-
# You also can pass a proc to #bind
|
28
|
-
|
29
|
-
add_two = -> (x) { Maybe(x + 2) }
|
30
|
-
|
31
|
-
Maybe(5).bind(add_two).bind(add_two) # => Some(9)
|
32
|
-
Maybe(nil).bind(add_two).bind(add_two) # => None()
|
33
|
-
|
34
|
-
```
|
35
|
-
|
36
|
-
### `fmap`
|
37
|
-
|
38
|
-
Similar to `bind` but works with blocks/methods that returns unwrapped values (i.e. not `Maybe` instances).
|
39
|
-
|
40
|
-
```ruby
|
41
|
-
extend Dry::Monads[:maybe]
|
42
|
-
|
43
|
-
Maybe(10).fmap { |x| x + 5 }.fmap { |y| y * 2 }
|
44
|
-
# => Some(30)
|
45
|
-
```
|
46
|
-
|
47
|
-
In 1.x `Maybe#fmap` coerces `nil` values returned from blocks to `None`. This behavior will be changed in 2.0. This will be done because implicit coercion violates the functor laws which in order can lead to a surpising (not in a good sense) behavior. If you expect a block to return `nil`, use `Maybe#maybe` added in 1.3.
|
48
|
-
|
49
|
-
### `maybe`
|
50
|
-
|
51
|
-
Almost identical to `Maybe#fmap` but maps `nil` to `None`. This is similar to how the `&.` operator works in Ruby but does wrapping:
|
52
|
-
|
53
|
-
```ruby
|
54
|
-
extend Dry::Monads[:maybe]
|
55
|
-
|
56
|
-
Maybe(user).maybe(&:address).maybe(&:street)
|
57
|
-
|
58
|
-
# If user with address exists
|
59
|
-
# => Some("Street Address")
|
60
|
-
# If user or address is nil
|
61
|
-
# => None()
|
62
|
-
```
|
63
|
-
|
64
|
-
### `value!`
|
65
|
-
|
66
|
-
You always can extract the result by calling `value!`. It will raise an error if you call it on `None`. You can use `value_or` for safe unwrapping.
|
67
|
-
|
68
|
-
```ruby
|
69
|
-
extend Dry::Monads[:maybe]
|
70
|
-
|
71
|
-
Some(5).fmap(&:succ).value! # => 6
|
72
|
-
|
73
|
-
None().fmap(&:succ).value!
|
74
|
-
# => Dry::Monads::UnwrapError: value! was called on None
|
75
|
-
|
76
|
-
```
|
77
|
-
|
78
|
-
### `value_or`
|
79
|
-
|
80
|
-
Has one argument, unwraps the value in case of `Some` or returns the argument value back in case of `None`. It's a safe and recommended way of extracting values.
|
81
|
-
|
82
|
-
```ruby
|
83
|
-
extend Dry::Monads[:maybe]
|
84
|
-
|
85
|
-
add_two = -> (x) { Maybe(x + 2) }
|
86
|
-
|
87
|
-
Maybe(5).bind(add_two).value_or(0) # => 7
|
88
|
-
Maybe(nil).bind(add_two).value_or(0) # => 0
|
89
|
-
|
90
|
-
Maybe(nil).bind(add_two).value_or { 0 } # => 0
|
91
|
-
```
|
92
|
-
|
93
|
-
### `or`
|
94
|
-
|
95
|
-
The opposite of `bind`.
|
96
|
-
|
97
|
-
```ruby
|
98
|
-
extend Dry::Monads[:maybe]
|
99
|
-
|
100
|
-
add_two = -> (x) { Maybe(x + 2) }
|
101
|
-
|
102
|
-
Maybe(5).bind(add_two).or(Some(0)) # => Some(7)
|
103
|
-
Maybe(nil).bind(add_two).or(Some(0)) # => Some(0)
|
104
|
-
|
105
|
-
Maybe(nil).bind(add_two).or { Some(0) } # => Some(0)
|
106
|
-
```
|
107
|
-
|
108
|
-
### `and`
|
109
|
-
|
110
|
-
Two values can be chained using `.and`:
|
111
|
-
|
112
|
-
```ruby
|
113
|
-
extend Dry::Monads[:maybe]
|
114
|
-
|
115
|
-
Some(5).and(Some(10)) { |x, y| x + y } # => Some(15)
|
116
|
-
Some(5).and(None) { |x, y| x + y } # => None()
|
117
|
-
None().and(Some(10)) { |x, y| x + y } # => None()
|
118
|
-
|
119
|
-
Some(5).and(Some(10)) # => Some([5, 10])
|
120
|
-
Some(5).and(None()) # => None()
|
121
|
-
```
|
122
|
-
|
123
|
-
### `flatten`
|
124
|
-
|
125
|
-
To remove one level of nesting:
|
126
|
-
|
127
|
-
```ruby
|
128
|
-
extend Dry::Monads[:maybe]
|
129
|
-
|
130
|
-
Some(Some(10)).flatten # => Some(10)
|
131
|
-
Some(None()).flatten # => None()
|
132
|
-
None().flatten # => None()
|
133
|
-
```
|
134
|
-
|
135
|
-
### `to_result`
|
136
|
-
|
137
|
-
Maybe values can be converted to Result objects:
|
138
|
-
|
139
|
-
```ruby
|
140
|
-
extend Dry::Monads[:maybe, :result]
|
141
|
-
|
142
|
-
Some(10).to_result # => Success(10)
|
143
|
-
None().to_result # => Failure()
|
144
|
-
None().to_result(:error) # => Failure(:error)
|
145
|
-
None().to_result { :block_value } # => Failure(:block_value)
|
146
|
-
```
|
@@ -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 matchings, 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)
|
49
|
-
# x is an 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
|
-
```
|