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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +157 -73
  3. data/LICENSE +1 -1
  4. data/README.md +18 -38
  5. data/dry-monads.gemspec +32 -30
  6. data/lib/dry-monads.rb +3 -1
  7. data/lib/dry/monads.rb +4 -2
  8. data/lib/dry/monads/all.rb +4 -2
  9. data/lib/dry/monads/constants.rb +1 -1
  10. data/lib/dry/monads/conversion_stubs.rb +2 -0
  11. data/lib/dry/monads/curry.rb +2 -0
  12. data/lib/dry/monads/do.rb +55 -17
  13. data/lib/dry/monads/do/all.rb +39 -17
  14. data/lib/dry/monads/do/mixin.rb +2 -0
  15. data/lib/dry/monads/either.rb +9 -7
  16. data/lib/dry/monads/errors.rb +8 -3
  17. data/lib/dry/monads/lazy.rb +19 -6
  18. data/lib/dry/monads/list.rb +31 -30
  19. data/lib/dry/monads/maybe.rb +90 -19
  20. data/lib/dry/monads/registry.rb +15 -12
  21. data/lib/dry/monads/result.rb +42 -15
  22. data/lib/dry/monads/result/fixed.rb +35 -24
  23. data/lib/dry/monads/right_biased.rb +45 -24
  24. data/lib/dry/monads/task.rb +25 -22
  25. data/lib/dry/monads/transformer.rb +4 -1
  26. data/lib/dry/monads/traverse.rb +9 -1
  27. data/lib/dry/monads/try.rb +51 -13
  28. data/lib/dry/monads/unit.rb +6 -2
  29. data/lib/dry/monads/validated.rb +27 -20
  30. data/lib/dry/monads/version.rb +3 -1
  31. data/lib/json/add/dry/monads/maybe.rb +4 -3
  32. metadata +27 -75
  33. data/.codeclimate.yml +0 -12
  34. data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +0 -10
  35. data/.github/ISSUE_TEMPLATE/---bug-report.md +0 -34
  36. data/.github/ISSUE_TEMPLATE/---feature-request.md +0 -18
  37. data/.github/workflows/ci.yml +0 -74
  38. data/.github/workflows/docsite.yml +0 -34
  39. data/.github/workflows/sync_configs.yml +0 -34
  40. data/.gitignore +0 -10
  41. data/.rspec +0 -4
  42. data/.rubocop.yml +0 -89
  43. data/.yardopts +0 -4
  44. data/CODE_OF_CONDUCT.md +0 -13
  45. data/CONTRIBUTING.md +0 -29
  46. data/Gemfile +0 -23
  47. data/Rakefile +0 -6
  48. data/bin/console +0 -16
  49. data/bin/setup +0 -7
  50. data/docsite/source/case-equality.html.md +0 -42
  51. data/docsite/source/do-notation.html.md +0 -207
  52. data/docsite/source/getting-started.html.md +0 -142
  53. data/docsite/source/index.html.md +0 -179
  54. data/docsite/source/list.html.md +0 -87
  55. data/docsite/source/maybe.html.md +0 -146
  56. data/docsite/source/pattern-matching.html.md +0 -68
  57. data/docsite/source/result.html.md +0 -190
  58. data/docsite/source/task.html.md +0 -126
  59. data/docsite/source/tracing-failures.html.md +0 -32
  60. data/docsite/source/try.html.md +0 -76
  61. data/docsite/source/unit.html.md +0 -36
  62. data/docsite/source/validated.html.md +0 -88
  63. data/log/.gitkeep +0 -0
@@ -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
- ```