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,142 +0,0 @@
1
- ---
2
- title: Getting started
3
- layout: gem-single
4
- name: dry-monads
5
- ---
6
-
7
- ### Installation
8
-
9
- Add this line to your Gemfile
10
-
11
- ```ruby
12
- gem 'dry-monads'
13
- ```
14
-
15
- Then run
16
-
17
- ```
18
- $ bundle
19
- ```
20
-
21
- ### Usage
22
-
23
- Every monad has corresponding value constructors. For example, the `Maybe` monad has two of them: `Some(...)` and `None()`. It also has the `Maybe(...)` method. All three methods start with a capital letter similarly to built-in Ruby methods like `Kernel#Array(...)` and `Kernel#Hash(...)`. Value constructors are not available globally, you need to add them with a mixin.
24
-
25
- To add the `Maybe` constructors add `Dry::Monads[:maybe]` to your class:
26
-
27
- ```ruby
28
- require 'dry/monads'
29
-
30
- class CreateUser
31
- # this line loads the Maybe monad and adds
32
- # Some(...), None(), and Maybe(...) to CreateUser
33
- include Dry::Monads[:maybe]
34
-
35
- def call(params)
36
- # ...
37
- if valid?(params)
38
- None()
39
- else
40
- Some(create_user(params))
41
- end
42
- end
43
- end
44
- ```
45
-
46
- Example in the docs may use `extend Dry::Monads[...]` for brevity but you normally want to use `include` in production code.
47
-
48
- ### Including multiple monads
49
-
50
- ```ruby
51
- require 'dry/monads'
52
-
53
- class CreateUser
54
- # Adds Maybe and Result. The order doesn't matter
55
- include Dry::Monads[:maybe, :result]
56
- end
57
- ```
58
-
59
- ### Using with do notation
60
-
61
- A very common case is using the [Result](docs::result) monad with [do notation](docs::do-notation):
62
-
63
- ```ruby
64
- require 'dry/monads'
65
-
66
- class ResultCalculator
67
- include Dry::Monads[:result, :do]
68
-
69
- def calculate(input)
70
- value = Integer(input)
71
-
72
- value = yield add_3(value)
73
- value = yield mult_2(value)
74
-
75
- Success(value)
76
- end
77
-
78
- def add_3(value)
79
- if value > 1
80
- Success(value + 3)
81
- else
82
- Failure("value was less than 1")
83
- end
84
- end
85
-
86
- def mult_2(value)
87
- if value % 2 == 0
88
- Success(value * 2)
89
- else
90
- Failure("value was not even")
91
- end
92
- end
93
- end
94
-
95
-
96
- c = ResultCalculator.new
97
- c.calculate(3) # => Success(12)
98
- c.calculate(0) # => Failure("value was less than 1")
99
- c.calculate(2) # => Failure("value was not even")
100
- ```
101
-
102
- ### Constructing array values
103
-
104
- Some constructors have shortcuts for wrapping arrays:
105
-
106
- ```ruby
107
- require 'dry/monads'
108
-
109
- class CreateUser
110
- include Dry::Monads[:result]
111
-
112
- def call(params)
113
- # ...
114
- # Same as Failure([:user_exists, params: params])
115
- Failure[:user_exists, params: params]
116
- end
117
- end
118
- ```
119
-
120
- ### Interaction between monads and constructors availability
121
-
122
- Some values can be converted to others or they can have methods that use other monads. By default, dry-monads doesn't load all monads so you may have troubles like this:
123
-
124
- ```ruby
125
- extend Dry::Monads[:result]
126
-
127
- Success(:foo).to_maybe # RuntimeError: Load Maybe first with require 'dry/monads/maybe'
128
- ```
129
-
130
- To work around you may either load `dry/monads/maybe` add `maybe` to the mixin:
131
-
132
- ```ruby
133
- extend Dry::Monads[:result, :maybe]
134
-
135
- Success(:foo).to_maybe # => Some(:foo)
136
- ```
137
-
138
- For the same reason `Dry::Monads.Some(...)`, `Dry::Monads.Success(...)`, and some other constructors are not available until you explicitly load the monads with `require 'dry/monads/%{monad_name}'`.
139
-
140
- ### Loading everything
141
-
142
- Just `require 'dry/monads/all'`
@@ -1,179 +0,0 @@
1
- ---
2
- title: Introduction
3
- description: Common monads for Ruby
4
- layout: gem-single
5
- type: gem
6
- name: dry-monads
7
- sections:
8
- - getting-started
9
- - maybe
10
- - result
11
- - do-notation
12
- - try
13
- - list
14
- - task
15
- - validated
16
- - case-equality
17
- - tracing-failures
18
- - pattern-matching
19
- - unit
20
- ---
21
-
22
- dry-monads is a set of common monads for Ruby. Monads provide an elegant way of handling errors, exceptions and chaining functions so that the code is much more understandable and has all the error handling, without all the `if`s and `else`s. The gem was inspired by the [Kleisli](https://github.com/txus/kleisli) gem.
23
-
24
- What is a monad, anyway? Simply, [a monoid in the category of endofunctors](https://stackoverflow.com/questions/3870088/a-monad-is-just-a-monoid-in-the-category-of-endofunctors-whats-the-proble%E2%85%BF). The term comes from category theory and some believe monads are tough to understand or explain. It's hard to say why people think so because you certainly don't need to know category theory for using them, just like you don't need it for, say, using functions.
25
-
26
- Moreover, the best way to develop intuition about monads is looking at examples rather than learning theories.
27
-
28
- ## How to use it?
29
-
30
- Let's say you have code like this
31
-
32
- ```ruby
33
- user = User.find_by(id: params[:id])
34
-
35
- if user
36
- address = user.address
37
- end
38
-
39
- if address
40
- city = address.city
41
- end
42
-
43
- if city
44
- state = city.state
45
- end
46
-
47
- if state
48
- state_name = state.name
49
- end
50
-
51
- user_state = state_name || "No state"
52
- ```
53
-
54
- Writing code in this style is tedious and error-prone. There were created several "cutting-corners" means to work around this issue. The first is ActiveSupport's `.try` which is a plain global monkey patch on `NilClass` and `Object`. Another solution is using the Safe Navigation Operator `&.` introduced in Ruby 2.3 which is a bit better because this is a language feature rather than an opinionated runtime environment pollution. However, some people think these solutions are hacks and the problem reveals a missing abstraction. What kind of abstraction?
55
-
56
- When all objects from the chain of objects are there we could have this instead:
57
-
58
- ```ruby
59
- state_name = User.find_by(id: params[:id]).address.city.state.name
60
- user_state = state_name || "No state"
61
- ```
62
-
63
- By using the `Maybe` monad you can preserve the structure of this code at a cost of introducing a notion of `nil`-able result:
64
-
65
- ```ruby
66
- state_name = Maybe(User.find_by(id: params[:id])).maybe(&:address).maybe(&:city).maybe(&:state).maybe(&:name)
67
- user_state = state_name.value_or("No state")
68
- ```
69
-
70
- `Maybe(...)` wraps the first value and returns a monadic value which either can be a `Some(user)` or `None` if `user` is `nil`. `maybe(&:address)` transforms `Some(user)` to `Some(address)` but leaves `None` intact. To get the final value you can use `value_or` which is a safe way to unwrap a `nil`-able value. In other words, once you've used `Maybe` you _cannot_ hit `nil` with a missing method. This is remarkable because even `&.` doesn't save you from omitting `|| "No state"` at the end of the computation. Basically, that's what they call "Type Safety".
71
-
72
- A more expanded example is based on _composing_ different monadic values. Suppose, we have a user and address, both can be `nil`, and we want to associate the address with the user:
73
-
74
- ```ruby
75
- user = User.find_by(id: params[:user_id])
76
- address = Address.find_by(id: params[:address_id])
77
-
78
- if user && address
79
- user.update(address_id: address.id)
80
- end
81
- ```
82
-
83
- Again, this implies direct work with `nil`-able values which may end up with errors. A monad-way would be using another method, `bind`:
84
-
85
- ```ruby
86
- maybe_user = Maybe(User.find_by(id: params[:user_id]))
87
-
88
- maybe_user.bind do |user|
89
- maybe_address = Maybe(Address.find_by(id: params[:address_id]))
90
-
91
- maybe_address.bind do |address|
92
- user.update(address_id: address.id)
93
- end
94
- end
95
- ```
96
-
97
- One can say this code is opaque compared to the previous example but keep in mind that in _real code_ it often happens to call methods returning `Maybe` values. In this case, it might look like this:
98
-
99
- ```ruby
100
- find_user(params[:user_id]).bind do |user|
101
- find_address(params[:address_id]).bind do |address|
102
- Some(user.update(address_id: address.id))
103
- end
104
- end
105
- ```
106
-
107
- Finally, since 1.0, dry-monads has support for [`do` notation](docs::do-notation) which simplifies this code even more, making it almost regular yet `nil`-safe:
108
-
109
- ```ruby
110
- user = yield find_user(params[:user_id])
111
- address = yield find_address(params[:address_id])
112
-
113
- Some(user.update(address_id: address.id))
114
- ```
115
-
116
- Another widely spread monad is `Result` (also known as `Either`) that serves a similar purpose. A notable downside of `Maybe` is plain `None` which carries no information about where this value was produced. `Result` solves exactly this problem by having two constructors for `Success` and `Failure` cases:
117
-
118
- ```ruby
119
- def find_user(user_id)
120
- user = User.find_by(id: user_id)
121
-
122
- if user
123
- Success(user)
124
- else
125
- Failure(:user_not_found)
126
- end
127
- end
128
-
129
- def find_address(address_id)
130
- address = Address.find_by(id: address_id)
131
-
132
- if address
133
- Success(address)
134
- else
135
- Failure(:address_not_found)
136
- end
137
- end
138
- ```
139
-
140
- You can compose `find_user` and `find_address` with `bind`:
141
-
142
- ```ruby
143
- find_user(params[:user_id]).bind do |user|
144
- find_address(params[:address_id]).bind |address|
145
- Success(user.update(address_id: address.id))
146
- end
147
- end
148
- ```
149
-
150
- The inner block can be simplified with `fmap`:
151
-
152
- ```ruby
153
- find_user(params[:user_id]).bind do |user|
154
- find_address(params[:address_id]).fmap |address|
155
- user.update(address_id: address.id)
156
- end
157
- end
158
- ```
159
-
160
- Or, again, the same code with `do`:
161
-
162
- ```ruby
163
- user = yield find_user(params[:user_id])
164
- address = yield find_address(params[:address_id])
165
-
166
- Success(user.update(address_id: address.id))
167
- ```
168
-
169
- The result of this piece of code can be one of `Success(user)`, `Failure(:user_not_found)`, or `Failure(:address_not_found)`. This style of programming called "Railway Oriented Programming" and can check out [dry-transaction](/gems/dry-transaction) and watch a [nice video](https://fsharpforfunandprofit.com/rop/) on the subject. Also, see [dry-matcher](/gems/dry-matcher) for an example of how to use monads for controlling the flow of code with a result.
170
-
171
- ## A word of warning
172
-
173
- Before `do` came around here was a warning about over-using monads, turned out with `do` notation code does not differ much from regular Ruby code. Just don't wrap everything with `Maybe`, come up with conventions.
174
-
175
- If you're interested in functional programming in general, consider learning other languages such as Haskell, Scala, OCaml, this will make you a better programmer no matter what programming language you use on a daily basis. And if not earlier then maybe after that dry-monads will become another instrument in your Ruby toolbox :)
176
-
177
- ## Credits
178
-
179
- dry-monads is inspired by Josep M. Bach’s [Kleisli](https://github.com/txus/kleisli) gem and its usage by [dry-transaction](/gems/dry-transaction/) and [dry-types](/gems/dry-types/).
@@ -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
- ```