dry-monads 1.3.2 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
-
```
|