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
@@ -1,42 +0,0 @@
|
|
1
|
-
---
|
2
|
-
title: Case equality
|
3
|
-
layout: gem-single
|
4
|
-
name: dry-monads
|
5
|
-
---
|
6
|
-
|
7
|
-
### Case equality
|
8
|
-
|
9
|
-
Monads allow to use default ruby `case` operator for matching result:
|
10
|
-
|
11
|
-
```ruby
|
12
|
-
case value
|
13
|
-
when Some(1), Some(2) then :one_or_two
|
14
|
-
when Some(3..5) then :three_to_five
|
15
|
-
else
|
16
|
-
:something_else
|
17
|
-
end
|
18
|
-
```
|
19
|
-
|
20
|
-
You can use specific `Failure` options too:
|
21
|
-
|
22
|
-
```ruby
|
23
|
-
case value
|
24
|
-
when Success then [:ok, value.value!]
|
25
|
-
when Failure(TimeoutError) then [:timeout]
|
26
|
-
when Failure(ConnectionClosed) then [:net_error]
|
27
|
-
when Failure then [:generic_error]
|
28
|
-
else
|
29
|
-
raise "Unhandled case"
|
30
|
-
end
|
31
|
-
```
|
32
|
-
|
33
|
-
#### Nested structures
|
34
|
-
|
35
|
-
```ruby
|
36
|
-
case value
|
37
|
-
when Success(None()) then :nothing
|
38
|
-
when Success(Some { |x| x > 10 }) then :something
|
39
|
-
when Success(Some) then :something_else
|
40
|
-
when Failure then :error
|
41
|
-
end
|
42
|
-
```
|
@@ -1,207 +0,0 @@
|
|
1
|
-
---
|
2
|
-
title: Do notation
|
3
|
-
layout: gem-single
|
4
|
-
name: dry-monads
|
5
|
-
---
|
6
|
-
|
7
|
-
Composing several monadic values can become tedious because you need to pass around unwrapped values in lambdas (aka blocks). Haskell was one of the first languages faced this problem. To work around it Haskell has a special syntax for combining monadic operations called the "do notation". If you're familiar with Scala it has `for`-comprehensions for a similar purpose. It is not possible to implement `do` in Ruby but it is possible to emulate it to some extent, i.e. achieve comparable usefulness.
|
8
|
-
|
9
|
-
What `Do` does is passing an unwrapping block to certain methods. The block tries to extract the underlying value from a monadic object and either short-circuits the execution (in case of a failure) or returns the unwrapped value back.
|
10
|
-
|
11
|
-
See the following example written using `bind` and `fmap`:
|
12
|
-
|
13
|
-
```ruby
|
14
|
-
require 'dry/monads'
|
15
|
-
|
16
|
-
class CreateAccount
|
17
|
-
include Dry::Monads[:result]
|
18
|
-
|
19
|
-
def call(params)
|
20
|
-
validate(params).bind { |values|
|
21
|
-
create_account(values[:account]).bind { |account|
|
22
|
-
create_owner(account, values[:owner]).fmap { |owner|
|
23
|
-
[account, owner]
|
24
|
-
}
|
25
|
-
}
|
26
|
-
}
|
27
|
-
end
|
28
|
-
|
29
|
-
def validate(params)
|
30
|
-
# returns Success(values) or Failure(:invalid_data)
|
31
|
-
end
|
32
|
-
|
33
|
-
def create_account(account_values)
|
34
|
-
# returns Success(account) or Failure(:account_not_created)
|
35
|
-
end
|
36
|
-
|
37
|
-
def create_owner(account, owner_values)
|
38
|
-
# returns Success(owner) or Failure(:owner_not_created)
|
39
|
-
end
|
40
|
-
end
|
41
|
-
```
|
42
|
-
|
43
|
-
The more monadic steps you need to combine the harder it becomes, not to mention how difficult it can be to refactor code written in such way.
|
44
|
-
|
45
|
-
Embrace `Do`:
|
46
|
-
|
47
|
-
```ruby
|
48
|
-
require 'dry/monads'
|
49
|
-
require 'dry/monads/do'
|
50
|
-
|
51
|
-
class CreateAccount
|
52
|
-
include Dry::Monads[:result]
|
53
|
-
include Dry::Monads::Do.for(:call)
|
54
|
-
|
55
|
-
def call(params)
|
56
|
-
values = yield validate(params)
|
57
|
-
account = yield create_account(values[:account])
|
58
|
-
owner = yield create_owner(account, values[:owner])
|
59
|
-
|
60
|
-
Success([account, owner])
|
61
|
-
end
|
62
|
-
|
63
|
-
def validate(params)
|
64
|
-
# returns Success(values) or Failure(:invalid_data)
|
65
|
-
end
|
66
|
-
|
67
|
-
def create_account(account_values)
|
68
|
-
# returns Success(account) or Failure(:account_not_created)
|
69
|
-
end
|
70
|
-
|
71
|
-
def create_owner(account, owner_values)
|
72
|
-
# returns Success(owner) or Failure(:owner_not_created)
|
73
|
-
end
|
74
|
-
end
|
75
|
-
```
|
76
|
-
|
77
|
-
Both snippets do the same thing yet the second one is a lot easier to deal with. All what `Do` does here is prepending `CreateAccount` with a module which passes a block to `CreateAccount#call`. That simple.
|
78
|
-
|
79
|
-
### Transaction safety
|
80
|
-
|
81
|
-
Under the hood, `Do` uses exceptions to halt unsuccessful operations, this can be slower if you are dealing with unsuccessful paths a lot, but usually, this is not an issue. Check out [this article](https://www.morozov.is/2018/05/27/do-notation-ruby.html) for actual benchmarks.
|
82
|
-
|
83
|
-
One particular reason to use exceptions is the ability to make code transaction-friendly. In the example above, this piece of code is not atomic:
|
84
|
-
|
85
|
-
```ruby
|
86
|
-
account = yield create_account(values[:account])
|
87
|
-
owner = yield create_owner(account, values[:owner])
|
88
|
-
|
89
|
-
Success[account, owner]
|
90
|
-
```
|
91
|
-
|
92
|
-
What if `create_account` succeeds and `create_owner` fails? This will leave your database in an inconsistent state. Let's wrap it with a transaction block:
|
93
|
-
|
94
|
-
```ruby
|
95
|
-
repo.transaction do
|
96
|
-
account = yield create_account(values[:account])
|
97
|
-
owner = yield create_owner(account, values[:owner])
|
98
|
-
|
99
|
-
Success[account, owner]
|
100
|
-
end
|
101
|
-
```
|
102
|
-
|
103
|
-
Since `yield` internally uses exceptions to control the flow, the exception will be detected by the `transaction` call and the whole operation will be rolled back. No more garbage in your database, yay!
|
104
|
-
|
105
|
-
### Limitations
|
106
|
-
|
107
|
-
`Do` only works with single-value monads, i.e. most of them. At the moment, there is no way to make it work with `List`, though.
|
108
|
-
|
109
|
-
### Adding batteries
|
110
|
-
|
111
|
-
The `Do::All` module takes one step ahead, it tracks all new methods defined in the class and passes a block to every one of them. However, if you pass a block yourself then it takes precedence. This way, in most cases you can use `Do::All` instead of listing methods with `Do.for(...)`:
|
112
|
-
|
113
|
-
```ruby
|
114
|
-
require 'dry/monads'
|
115
|
-
|
116
|
-
class CreateAccount
|
117
|
-
# This will include Do::All by default
|
118
|
-
include Dry::Monads[:result, :do]
|
119
|
-
|
120
|
-
def call(account_params, owner_params)
|
121
|
-
repo.transaction do
|
122
|
-
account = yield create_account(account_params)
|
123
|
-
owner = yield create_owner(account, owner_params)
|
124
|
-
|
125
|
-
Success[account, owner]
|
126
|
-
end
|
127
|
-
end
|
128
|
-
|
129
|
-
def create_account(params)
|
130
|
-
values = yield validate_account(params)
|
131
|
-
account = repo.create_account(values)
|
132
|
-
|
133
|
-
Success(account)
|
134
|
-
end
|
135
|
-
|
136
|
-
def create_owner(account, params)
|
137
|
-
values = yield validate_owner(params)
|
138
|
-
owner = repo.create_owner(account, values)
|
139
|
-
|
140
|
-
Success(owner)
|
141
|
-
end
|
142
|
-
|
143
|
-
def validate_account(params)
|
144
|
-
# returns Success/Failure
|
145
|
-
end
|
146
|
-
|
147
|
-
def validate_owner(params)
|
148
|
-
# returns Success/Failure
|
149
|
-
end
|
150
|
-
end
|
151
|
-
```
|
152
|
-
|
153
|
-
### Using `Do` methods in other contexts
|
154
|
-
|
155
|
-
You can use methods from the `Do` module directly (starting with 1.3):
|
156
|
-
|
157
|
-
```ruby
|
158
|
-
require 'dry/monads/do'
|
159
|
-
require 'dry/monads/result'
|
160
|
-
|
161
|
-
# some random place in your code
|
162
|
-
Dry::Monads.Do.() do
|
163
|
-
user = Dry::Monads::Do.bind create_user
|
164
|
-
account = Dry::Monads::Do.bind create_account(user)
|
165
|
-
|
166
|
-
Dry::Monads::Success[user, account]
|
167
|
-
end
|
168
|
-
```
|
169
|
-
|
170
|
-
Or you can use `extend`:
|
171
|
-
|
172
|
-
```ruby
|
173
|
-
require 'dry/monads'
|
174
|
-
|
175
|
-
class VeryComplexAndUglyCode
|
176
|
-
extend Dry::Monads::Do::Mixin
|
177
|
-
extend Dry::Monads[:result]
|
178
|
-
|
179
|
-
def self.create_something(result_value)
|
180
|
-
call do
|
181
|
-
extracted = bind result_value
|
182
|
-
processed = bind process(extracted)
|
183
|
-
|
184
|
-
Success(processed)
|
185
|
-
end
|
186
|
-
end
|
187
|
-
end
|
188
|
-
```
|
189
|
-
|
190
|
-
`Do::All` also works with class methods:
|
191
|
-
|
192
|
-
```ruby
|
193
|
-
require 'dry/monads'
|
194
|
-
|
195
|
-
class SomeClassLevelLogic
|
196
|
-
extend Dry::Monads[:result, :do]
|
197
|
-
|
198
|
-
def self.call
|
199
|
-
x = yield Success(5)
|
200
|
-
y = yield Success(20)
|
201
|
-
|
202
|
-
Success(x * y)
|
203
|
-
end
|
204
|
-
end
|
205
|
-
|
206
|
-
SomeClassLevelLogic.() # => Success(100)
|
207
|
-
```
|
@@ -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/).
|