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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +10 -39
  3. data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +10 -0
  4. data/.github/ISSUE_TEMPLATE/---bug-report.md +34 -0
  5. data/.github/ISSUE_TEMPLATE/---feature-request.md +18 -0
  6. data/.github/workflows/ci.yml +74 -0
  7. data/.github/workflows/docsite.yml +34 -0
  8. data/.github/workflows/sync_configs.yml +34 -0
  9. data/.rspec +1 -0
  10. data/.rubocop.yml +89 -0
  11. data/CHANGELOG.md +21 -0
  12. data/CODE_OF_CONDUCT.md +13 -0
  13. data/CONTRIBUTING.md +4 -4
  14. data/Gemfile +4 -1
  15. data/LICENSE +17 -17
  16. data/README.md +2 -2
  17. data/docsite/source/case-equality.html.md +42 -0
  18. data/docsite/source/do-notation.html.md +207 -0
  19. data/docsite/source/getting-started.html.md +142 -0
  20. data/docsite/source/index.html.md +179 -0
  21. data/docsite/source/list.html.md +87 -0
  22. data/docsite/source/maybe.html.md +146 -0
  23. data/docsite/source/pattern-matching.html.md +68 -0
  24. data/docsite/source/result.html.md +190 -0
  25. data/docsite/source/task.html.md +126 -0
  26. data/docsite/source/tracing-failures.html.md +32 -0
  27. data/docsite/source/try.html.md +76 -0
  28. data/docsite/source/unit.html.md +36 -0
  29. data/docsite/source/validated.html.md +88 -0
  30. data/lib/dry/monads.rb +1 -0
  31. data/lib/dry/monads/{undefined.rb → constants.rb} +3 -1
  32. data/lib/dry/monads/do.rb +2 -1
  33. data/lib/dry/monads/do/all.rb +2 -2
  34. data/lib/dry/monads/do/mixin.rb +2 -3
  35. data/lib/dry/monads/maybe.rb +2 -4
  36. data/lib/dry/monads/result.rb +1 -1
  37. data/lib/dry/monads/right_biased.rb +52 -7
  38. data/lib/dry/monads/validated.rb +1 -1
  39. data/lib/dry/monads/version.rb +1 -1
  40. metadata +25 -5
  41. data/.travis.yml +0 -38
@@ -0,0 +1,179 @@
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/).
@@ -0,0 +1,87 @@
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
+ ```
@@ -0,0 +1,146 @@
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
+ ```
@@ -0,0 +1,68 @@
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
+ ```