mayak 0.0.2
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 +7 -0
- data/README.md +33 -0
- data/lib/mayak/cache.rb +35 -0
- data/lib/mayak/caching/README.md +100 -0
- data/lib/mayak/caching/lru_cache.rb +76 -0
- data/lib/mayak/caching/unbounded_cache.rb +51 -0
- data/lib/mayak/collections/README.md +62 -0
- data/lib/mayak/collections/priority_queue.rb +155 -0
- data/lib/mayak/collections/queue.rb +73 -0
- data/lib/mayak/function.rb +46 -0
- data/lib/mayak/http/README.md +105 -0
- data/lib/mayak/http/client.rb +17 -0
- data/lib/mayak/http/codec.rb +23 -0
- data/lib/mayak/http/decoder.rb +43 -0
- data/lib/mayak/http/encoder.rb +55 -0
- data/lib/mayak/http/request.rb +110 -0
- data/lib/mayak/http/response.rb +29 -0
- data/lib/mayak/http/verb.rb +20 -0
- data/lib/mayak/json.rb +33 -0
- data/lib/mayak/monads/README.md +1409 -0
- data/lib/mayak/monads/maybe.rb +449 -0
- data/lib/mayak/monads/result.rb +574 -0
- data/lib/mayak/monads/try.rb +619 -0
- data/lib/mayak/numeric.rb +44 -0
- data/lib/mayak/predicates/rule.rb +74 -0
- data/lib/mayak/random.rb +15 -0
- data/lib/mayak/version.rb +6 -0
- data/lib/mayak/weak_ref.rb +37 -0
- data/lib/mayak.rb +35 -0
- data/mayak.gemspec +21 -0
- metadata +127 -0
@@ -0,0 +1,1409 @@
|
|
1
|
+
# Monads
|
2
|
+
|
3
|
+
## Table of Contents
|
4
|
+
* [Description](#description)
|
5
|
+
* [Maybe](#maybe)
|
6
|
+
* [Try](#try)
|
7
|
+
* [Result](#result)
|
8
|
+
|
9
|
+
## Description
|
10
|
+
|
11
|
+
This module is introduced to replace `dry-monads` with custom monads implementation which plays better with sorbet
|
12
|
+
type-checking. If you haven't worked with monads and don't know what's that, check [dry-monads documentation](https://dry-rb.org/gems/dry-monads/1.3/) first.
|
13
|
+
This should help you to get an idea of monads.
|
14
|
+
|
15
|
+
Now let's dive deeper into different kinds of monads.
|
16
|
+
|
17
|
+
## Maybe
|
18
|
+
|
19
|
+
This is probably the simplest monad that represents a series of computations could return `nil` at any point.
|
20
|
+
This monad is [parameterized](https://sorbet.org/docs/generics) with a type of value. Basically, the `Maybe` is supertype
|
21
|
+
of two subtypes: `Some` and `None` that shares the same interface. `Some` subtype contains a value, `None` doesn't contain
|
22
|
+
anything and represents `nil`.
|
23
|
+
|
24
|
+
### Initialization
|
25
|
+
|
26
|
+
The monad can be created with two primary constructors `Maybe` and `None` (note that these are methods).
|
27
|
+
Method `#Maybe` wraps a nilable value with a `Maybe` class: if a value is nil, than it returns value of `None`,
|
28
|
+
if the value is present, it returns instance of `Some`. In order to access these helpers`Mayak::Monads::Maybe::Mixin` must be included first:
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
include Mayak::Monads::Maybe::Mixin
|
32
|
+
|
33
|
+
sig { params(numbers: T::Array[Integer]).returns(T.nilable(Integer)) }
|
34
|
+
def first(numbers)
|
35
|
+
numbers.first
|
36
|
+
end
|
37
|
+
|
38
|
+
Maybe(first[]) # None
|
39
|
+
Maybe(first[1]) # Some[Integer](value = 1)
|
40
|
+
```
|
41
|
+
|
42
|
+
Also, the monad can be instantiated directly with `Mayak::Monads::Maybe::Some` and `Mayak::Monads::Maybe::None`:
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
sig { returns(Mayak::Monads::Maybe[Integer]) }
|
46
|
+
def some
|
47
|
+
Mayak::Monads::Maybe::Some[Integer].new(10)
|
48
|
+
end
|
49
|
+
|
50
|
+
sig { returns(Mayak::Monads::Maybe[Integer]) }
|
51
|
+
def none
|
52
|
+
Mayak::Monads::Maybe::None[Integer].new
|
53
|
+
end
|
54
|
+
```
|
55
|
+
|
56
|
+
### Unpacking
|
57
|
+
|
58
|
+
In order to retrieve a value from a `Maybe` a method `#value` which is defined on `Mayak::Monads::Maybe::Some` can be used.
|
59
|
+
`Mayak::Monads::Maybe::Some` is a subtask of `Maybe`. Note that the accessor is only defined on `Some` subtype, so if the `#value`
|
60
|
+
method will be called on an instance of `Maybe`, Sorbet will not type-check, since `Maybe` doesn't have this method.
|
61
|
+
In order to access value of `Maybe`, it need to be verified first that the monad is instance of `Some`, and only after that
|
62
|
+
this method can invoked. The most convenient way to do this is to use case-when statement.
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
sig { params(a: Integer, b: Integer).returns(Mayak::Monads::Maybe[Integer]) }
|
66
|
+
def divide(a, b)
|
67
|
+
if b == 0
|
68
|
+
None()
|
69
|
+
else
|
70
|
+
Maybe(a / b)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
sig { params(a: Integer, b: Integer).void }
|
75
|
+
def print_result_divide(a, b)
|
76
|
+
result = divide(a, b)
|
77
|
+
case result
|
78
|
+
when Mayak::Monads::Maybe::Some
|
79
|
+
# sorbet's flow-typing down-casts result to Mayak::Monads::Maybe::Some
|
80
|
+
# so type of this variable is Mayak::Monads::Maybe::Some in this branch
|
81
|
+
puts "#{a} / #{b} = #{result.value}"
|
82
|
+
when Mayak::Monads::Maybe::None
|
83
|
+
puts "Division by zero"
|
84
|
+
else
|
85
|
+
T.absurd(result)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
print_result_divide(10, 2) # 10 / 2 = 5
|
90
|
+
print_result_divide(10, 0) # Division by zero
|
91
|
+
```
|
92
|
+
|
93
|
+
The other way is to use method `#value_or` which returns either a value if the monad is `Some`, or
|
94
|
+
fallback value if the monad is `None`:
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
divide(10, 2).value_or(0) # 5
|
98
|
+
divide(10, 0).value_or(0) # 0
|
99
|
+
```
|
100
|
+
|
101
|
+
### Methods
|
102
|
+
|
103
|
+
#### `#to_dry`
|
104
|
+
|
105
|
+
Converts an instance of a monad in to an instance of corresponding dry-monad.
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
include Mayak::Monads::Maybe::Mixin
|
109
|
+
|
110
|
+
Maybe(10).to_dry # Some(10): Dry::Monads::Maybe::Some
|
111
|
+
Maybe(nil).to_dry # None: Dry::Monads::Maybe::None
|
112
|
+
```
|
113
|
+
|
114
|
+
#### `.from_dry`
|
115
|
+
|
116
|
+
Converts instance of `Dry::Monads::Maybe` into instance `Mayak::Monads::Maybe`
|
117
|
+
|
118
|
+
```ruby
|
119
|
+
include Mayak::Monads
|
120
|
+
|
121
|
+
Maybe.from_dry(Dry::Monads::Some(10)) # Some[Integer](value = 20)
|
122
|
+
Maybe.from_dry(Dry::Monads::None()) # None
|
123
|
+
```
|
124
|
+
|
125
|
+
#### `#map`
|
126
|
+
|
127
|
+
The same as `fmap` in a dry-monads `Maybe`. Allows to modify the value with a block if it's present.
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
sig { returns(Mayak::Monads::Maybe[Integer]) }
|
131
|
+
def some
|
132
|
+
Mayak::Monads::Maybe::Some[Integer].new(10)
|
133
|
+
end
|
134
|
+
|
135
|
+
sig { returns(Mayak::Monads::Maybe[Integer]) }
|
136
|
+
def none
|
137
|
+
Mayak::Monads::Maybe::None[Integer].new
|
138
|
+
end
|
139
|
+
|
140
|
+
some.map { |a| a + 20 } # Some[Integer](value = 20)
|
141
|
+
none.map { |a| a + 20 } # None[Integer]
|
142
|
+
```
|
143
|
+
|
144
|
+
#### `#flat_map`
|
145
|
+
The same as `bind` in a dry-monads `Maybe`. Allows to modify the value with a block that returns another `Maybe`
|
146
|
+
if it's present, otherwise returns `None`. If the block returns `None`, the whole computation returns `None`.
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
sig { params(a: Integer, b: Integer).returns(Mayak::Monads::Maybe[Integer]) }
|
150
|
+
def divide(a, b)
|
151
|
+
if b.zero?
|
152
|
+
Mayak::Monads::Maybe::None[Integer].new
|
153
|
+
else
|
154
|
+
Mayak::Monads::Maybe::Some[Integer].new(a / b)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
divide(20, 2).flat_map { |a| divide(a, 2) } # Some[Integer](value = 5)
|
159
|
+
divide(20, 2).flat_map { |a| divide(a, 0) } # None
|
160
|
+
divide(20, 0).flat_map { |a| divide(a, 2) } # None
|
161
|
+
```
|
162
|
+
|
163
|
+
#### `#filter`
|
164
|
+
|
165
|
+
Receives a block the returns a boolean value and checks the underlying value with the block when a monad is `Some`.
|
166
|
+
Returns `None` if the block called on the value returns `false`, and returns `self` if the block returns `true`.
|
167
|
+
Returns `None` if the monad is `None`.
|
168
|
+
|
169
|
+
```ruby
|
170
|
+
divide(20, 2).filter { |value| value > 5 } # Some[Integer](value = 10)
|
171
|
+
divide(20, 2).filter { |value| value < 5 } # None
|
172
|
+
divide(20, 0).filter { |value| value < 5 } # None
|
173
|
+
```
|
174
|
+
|
175
|
+
#### `#some?`
|
176
|
+
|
177
|
+
Returns true if a `Maybe` is a `Some` and false if it's `None`.
|
178
|
+
|
179
|
+
```ruby
|
180
|
+
divide(20, 2).some? # true
|
181
|
+
divide(20, 0).some? # false
|
182
|
+
```
|
183
|
+
|
184
|
+
#### `#none?`
|
185
|
+
|
186
|
+
Returns true if a `Maybe` is a `None` and false if it's `Some`.
|
187
|
+
|
188
|
+
```ruby
|
189
|
+
divide(20, 2).none? # false
|
190
|
+
divide(20, 0).none? # true
|
191
|
+
```
|
192
|
+
|
193
|
+
#### `#value_or`
|
194
|
+
|
195
|
+
Unpack a `Maybe` and returns its value if it's a `Some`, or returns provided fallback value if it's a `None`.
|
196
|
+
|
197
|
+
```ruby
|
198
|
+
divide(20, 2).value_or(0) # 10
|
199
|
+
divide(20, 0).value_or(0) # 0
|
200
|
+
```
|
201
|
+
|
202
|
+
#### `#to_task`
|
203
|
+
|
204
|
+
Converts a value to task. If a `Maybe` is an instance of `Some`, it returns succeeded task, if it's a `None`,
|
205
|
+
it returns failed task with a provided error.
|
206
|
+
|
207
|
+
```ruby
|
208
|
+
task = Mayak::Concurrent::Task.execute { 100 }
|
209
|
+
error = StandardError.new("Division by zero")
|
210
|
+
task.flat_map { |value| divide(value, 10).to_task(error) }.await! # 10
|
211
|
+
task.flat_map { |value| divide(value, 0).to_task(error) }.await! # StandardError: Divison by zero
|
212
|
+
```
|
213
|
+
|
214
|
+
#### `#to_result`
|
215
|
+
|
216
|
+
Converts a `Maybe` into a `Result`. If the `Maybe` is a `Some`, returns `Result::Success` with a value of the `Maybe`.
|
217
|
+
If it's a `None`, returns `Result::Failre` with a value of an error provided as an argument.
|
218
|
+
|
219
|
+
```ruby
|
220
|
+
divide(10, 2).to_result("Division by zero") # Success: Result[String, Integer](value = 5)
|
221
|
+
divide(10, 0).to_result("Division by zero") # Failure: Result[String, Integer](error = "Division by zero")
|
222
|
+
```
|
223
|
+
|
224
|
+
#### `#to_try`
|
225
|
+
|
226
|
+
Converts a `Maybe` into a `Try`. If the `Maybe` is a `Some`, returns `Try::Success` with a value of the `Maybe`.
|
227
|
+
If it's a `None`, returns `Try::Failre` with a value of an error provided as an argument.
|
228
|
+
|
229
|
+
```ruby
|
230
|
+
divide(10, 2).to_try(StandardError.new("Division by zero")) # Success: Try[Integer](value = 5)
|
231
|
+
divide(10, 0).to_try(StandardError.new("Division by zero")) # Failure: Try[Integer](error = StandardError(message = "Division by zero"))
|
232
|
+
```
|
233
|
+
|
234
|
+
Combination of `Maybe` constructor with `#to_try` method allows to significantly simplify common
|
235
|
+
pattern: checking nullability of a value, and returning error if the value is missing:
|
236
|
+
```ruby
|
237
|
+
sig { params(user: User).returns(Try[Address]) }
|
238
|
+
def get_address(user)
|
239
|
+
address = user.contact.address
|
240
|
+
|
241
|
+
if address.present?
|
242
|
+
Success(address)
|
243
|
+
else
|
244
|
+
Failure(AddressMissingError.new("Missing address for User(id=#{user.id})"))
|
245
|
+
end
|
246
|
+
end
|
247
|
+
```
|
248
|
+
|
249
|
+
With `Maybe` and `#to_try`:
|
250
|
+
|
251
|
+
```ruby
|
252
|
+
sig { params(user: User).returns(Try[Address]) }
|
253
|
+
def get_address(user)
|
254
|
+
Maybe(user.contact.address).to_try(
|
255
|
+
AddressMissingError.new("Missing address for User(id=#{user.id})")
|
256
|
+
)
|
257
|
+
end
|
258
|
+
```
|
259
|
+
|
260
|
+
#### `#tee`
|
261
|
+
|
262
|
+
If a `Maybe` is an instance of `Some`, runs a block with a value of `Some` passed, and returns the monad itself
|
263
|
+
unchanged. Doesn't do anything if it's a `None`.
|
264
|
+
|
265
|
+
```ruby
|
266
|
+
divide(10, 2).tee { |a| puts a }
|
267
|
+
# returns: Some[Integer](value = 5)
|
268
|
+
# console: 5
|
269
|
+
|
270
|
+
divide(10, 0).tee { |a| puts a }
|
271
|
+
# returns: None
|
272
|
+
```
|
273
|
+
|
274
|
+
Can be useful to embed side-effects into chain of monad transformation:
|
275
|
+
```ruby
|
276
|
+
sig { params(a: Integer, b: Integer).returns(Maybe[String]) }
|
277
|
+
def run(a, b)
|
278
|
+
divide(a, b)
|
279
|
+
.tee { |value| logger.info("#{a} / #{b} = #{value}") }
|
280
|
+
.map { |value| value * 100 }
|
281
|
+
.tee { |value| logger.info("Intermediate result = #{value}") }
|
282
|
+
.map(&:to_s)
|
283
|
+
end
|
284
|
+
```
|
285
|
+
|
286
|
+
#### `#recover`
|
287
|
+
|
288
|
+
Converts `None` into a `Some` with a provided value. If the monad is an instance of `Some`, returns itself.
|
289
|
+
|
290
|
+
```ruby
|
291
|
+
divide(10, 2).recover(0) # Some[Integer](value = 5)
|
292
|
+
divide(10, 0).recover(0) # Some[Integer](value = 0)
|
293
|
+
```
|
294
|
+
|
295
|
+
#### `.sequence`
|
296
|
+
`Maybe.sequence` takes an array of `Maybe`s and transform it into a `Maybe` of an array.
|
297
|
+
If all elements of an argument array is `Some`, then result will be `Some` of the array, otherwise
|
298
|
+
it will `None`.
|
299
|
+
|
300
|
+
```ruby
|
301
|
+
values = [Maybe(1), Maybe(2), Maybe(3)]
|
302
|
+
Maybe.sequence(values) # Some([10, 5, 3])
|
303
|
+
|
304
|
+
values = values = [Maybe(nil), Maybe(2), Maybe(3)]
|
305
|
+
Maybe.sequence(values) # None
|
306
|
+
```
|
307
|
+
|
308
|
+
#### `.check`
|
309
|
+
|
310
|
+
Receives a value and block returning a boolean value. If the block returns true,
|
311
|
+
the method returns the value wrapped in `Some`, otherwise it returns `None`:
|
312
|
+
|
313
|
+
```ruby
|
314
|
+
Maybe.check(10) { 20 > 10 } # Some[Integer](10)
|
315
|
+
Maybe.check(20) { 10 > 20 } # None
|
316
|
+
```
|
317
|
+
|
318
|
+
#### `.guard`
|
319
|
+
|
320
|
+
Receives a block returning a boolean value. If the block returns true,
|
321
|
+
the method returns a `Some` containing `nil`, otherwise `None` is returned:
|
322
|
+
|
323
|
+
```ruby
|
324
|
+
Maybe.guard { 20 > 10 } # Some[NilClass](nil)
|
325
|
+
Maybe.guard { 10 > 20 } # None
|
326
|
+
```
|
327
|
+
|
328
|
+
### Do-notation
|
329
|
+
|
330
|
+
Using `map` and `flat_map` for monads chaining can be really tedious, especially when computation
|
331
|
+
requires combining different values from branches.
|
332
|
+
|
333
|
+
Let's take a look at the following code snippet.
|
334
|
+
|
335
|
+
```ruby
|
336
|
+
sig { abstract.params(id: Integer).returns(Maybe[User]) }
|
337
|
+
def fetch_user(id)
|
338
|
+
end
|
339
|
+
|
340
|
+
sig { abstract.params(city: String, address: Address).returns(Maybe[Coordinates]) }
|
341
|
+
def fetch_city_coordinates(city, address)
|
342
|
+
end
|
343
|
+
|
344
|
+
sig { abstract.params(user: User, coordinates: Coordinates).returns(Maybe[UserAddressCache]) }
|
345
|
+
def fetch_user_address_cache(user, coordinates)
|
346
|
+
end
|
347
|
+
|
348
|
+
sig {
|
349
|
+
abstract
|
350
|
+
.params(user: User, address: Address, coordinates: Coordinates, cache: UserAddressCache)
|
351
|
+
.returns(Maybe[UserAddressData])
|
352
|
+
}
|
353
|
+
def build_user_address_data(user, address, coordinates, cache)
|
354
|
+
end
|
355
|
+
|
356
|
+
sig { params(user_id: Integer).returns(Maybe[UserAddressData]) }
|
357
|
+
def run(user_id)
|
358
|
+
fetch_user(user_id).flat_map { |user|
|
359
|
+
user
|
360
|
+
.address
|
361
|
+
.flat_map { |address|
|
362
|
+
address
|
363
|
+
.city
|
364
|
+
.flat_map { |city| fetch_city_coordinates(city) }
|
365
|
+
.flat_map { |coordinates|
|
366
|
+
fetch_user_address_cache(user, coordinates).flat_map { |cache|
|
367
|
+
build_user_address_data(user, address, coordinates, cache)
|
368
|
+
}
|
369
|
+
}
|
370
|
+
}
|
371
|
+
}
|
372
|
+
end
|
373
|
+
```
|
374
|
+
|
375
|
+
Don't worry if you can't really understand what's going on here, the code is intentionally overcomplicated to show you
|
376
|
+
to which extremes `#map` and `#flat_map` can lead.
|
377
|
+
|
378
|
+
In order to simplify computation you can use Do-notation. Let's see the same `run` method with using
|
379
|
+
Do-notation instead of `#map` and `#flat_map` chaining, and let's break down how it's work:
|
380
|
+
|
381
|
+
```ruby
|
382
|
+
# Make sure you have the Mixin included
|
383
|
+
include Mayak::Monads::Maybe::Mixin
|
384
|
+
|
385
|
+
sig { params(user_id: Integer).returns(Maybe[UserAddressCache]) }
|
386
|
+
def run(user_id)
|
387
|
+
for_maybe {
|
388
|
+
user = do_maybe! fetch_user(user_id)
|
389
|
+
address = do_maybe! user.address
|
390
|
+
city = do_maybe! address.city
|
391
|
+
coordinates = do_maybe! fetch_city_coordinates(city)
|
392
|
+
cache = do_maybe! fetch_user_address_cache(user, coordinates)
|
393
|
+
do_maybe! build_user_address_data(user, address, coordinates, cache)
|
394
|
+
}
|
395
|
+
end
|
396
|
+
```
|
397
|
+
|
398
|
+
Do-notation basically consists from two methods `for_maybe` and `do_maybe!`. `for_maybe` creates
|
399
|
+
a Do-notation scope within which you can use `do_maybe!` to unpack Monad values. If `do_maybe!` receives
|
400
|
+
a `Some` value, it unpacks it and returns underlying values, if it receives a `None`, it short circuits execution,
|
401
|
+
and the whole `for_maybe` block returns `None`. Otherwise `for_maybe` returns result of the last
|
402
|
+
expression wrapped into `Maybe`.
|
403
|
+
|
404
|
+
Let's check a few examples:
|
405
|
+
|
406
|
+
```ruby
|
407
|
+
result = for_maybe {
|
408
|
+
a = do_maybe! Maybe(10)
|
409
|
+
puts a # Prints: 10
|
410
|
+
b = do_maybe! Maybe(20)
|
411
|
+
puts b # Prints: 20
|
412
|
+
a + b
|
413
|
+
}
|
414
|
+
result # Some[Integer](value = 30)
|
415
|
+
|
416
|
+
failure = for_maybe {
|
417
|
+
a = do_maybe! Maybe(10)
|
418
|
+
b = do_maybe! None # stops execution here
|
419
|
+
puts "Not getting here" # Doesn't print anything
|
420
|
+
a + b
|
421
|
+
}
|
422
|
+
failure # None
|
423
|
+
```
|
424
|
+
You can also you methods `check_maybe!` and `guard_maybe!` to assert some invariants
|
425
|
+
in do-notation blocks. These methods are specialized helpers of `Maybe.check` and `Maybe.guard` for
|
426
|
+
more convenient usage in do-notation blocks:
|
427
|
+
|
428
|
+
```ruby
|
429
|
+
user = for_maybe {
|
430
|
+
user = do_maybe! fetch_user(user_id)
|
431
|
+
company = do_maybe! fetch_company(company_id)
|
432
|
+
# abrupt execution if block returns false
|
433
|
+
# or return user if block returns false
|
434
|
+
#
|
435
|
+
# semantically equivalent to:
|
436
|
+
# if user.works_in?(company)
|
437
|
+
# do_maybe! Some(user)
|
438
|
+
# else
|
439
|
+
# do_maybe! None.new
|
440
|
+
# end
|
441
|
+
check_maybe!(user) { user.works_in?(company) }
|
442
|
+
}
|
443
|
+
|
444
|
+
for_maybe {
|
445
|
+
api_key = do_maybe! Maybe(params[:api_key])
|
446
|
+
# abrupt execution if block returns None
|
447
|
+
#
|
448
|
+
# semantically equivalent to:
|
449
|
+
# if validate_api_key(api_key)
|
450
|
+
# do_maybe! Some(nil)
|
451
|
+
# else
|
452
|
+
# do_maybe! None.new
|
453
|
+
# end
|
454
|
+
guard_maybe! { validate_api_key(api_key) }
|
455
|
+
|
456
|
+
perform_query
|
457
|
+
}
|
458
|
+
```
|
459
|
+
|
460
|
+
A major improvement upon `dry-monads` do-notation, is that `Mayak::Monad`s do-notation if fully typed.
|
461
|
+
Do-notations infers both result type of the whole computation, and a type of a value unwrapped by `do_maybe!`
|
462
|
+
|
463
|
+
```ruby
|
464
|
+
for_maybe {
|
465
|
+
value = do_maybe! Maybe(10)
|
466
|
+
T.reveal_type(value)
|
467
|
+
value
|
468
|
+
}
|
469
|
+
# > Revealed type: Integer
|
470
|
+
|
471
|
+
result = for_maybe {
|
472
|
+
value = do_maybe! Maybe(10)
|
473
|
+
value
|
474
|
+
}
|
475
|
+
T.reveal_type(result)
|
476
|
+
# > Revealed type: Integer
|
477
|
+
```
|
478
|
+
|
479
|
+
## Try
|
480
|
+
|
481
|
+
`Try` monad represents result of a computation that can succeed with an arbitrary value, or fail with an error of a subtype of `StandardError`.
|
482
|
+
`Try` has two subtypes: `Failure` which contains an instance of subtype of `StandardError` and represents failure case,
|
483
|
+
and `Success`, which contains a success value of given type.
|
484
|
+
|
485
|
+
### Initialization
|
486
|
+
|
487
|
+
The primary way to create an instance of `Try` is to use constructor method `#Try` from `Mayak::Monads::Try::Mixin`.
|
488
|
+
This method receives a block that may raise exceptions. If an exception has been raised inside the block,
|
489
|
+
the method will return instance of `Try::Failure` containing an error. Otherwise the method will return result of the block
|
490
|
+
wrapped into `Try::Success`
|
491
|
+
|
492
|
+
```ruby
|
493
|
+
include Mayak::Monads::Try::Mixin
|
494
|
+
|
495
|
+
Try { 10 } # Try::Success[Integer](@value=10)
|
496
|
+
Try {
|
497
|
+
a = "Hello "
|
498
|
+
b = "World!"
|
499
|
+
a + b
|
500
|
+
} # Try::Success[String](@value="Hello World!")
|
501
|
+
|
502
|
+
Try {
|
503
|
+
a = 10
|
504
|
+
b = 0
|
505
|
+
a / b
|
506
|
+
} # Try::Failure[Integer](@failure=#<ZeroDivisionError: divided by 0>)
|
507
|
+
```
|
508
|
+
|
509
|
+
Note that constructor is able to infer a type of value from type returned from the block:
|
510
|
+
|
511
|
+
```ruby
|
512
|
+
sig { params(a: Integer, b: Integer).returns(Try[Integer]) }
|
513
|
+
def divide(a, b)
|
514
|
+
Try { a / b }
|
515
|
+
end
|
516
|
+
```
|
517
|
+
|
518
|
+
Exception should be an instance of `StandardError`s subtype to be captured:
|
519
|
+
```ruby
|
520
|
+
Try { raise Exception.new("Not going to be captured") }
|
521
|
+
# > Exception: Not going to be captured
|
522
|
+
```
|
523
|
+
|
524
|
+
`#Try` can receive types of arguments to be captured. If exceptions of other types were raised, they won't
|
525
|
+
captured. Types of exceptions should be subtypes of `StandardError`:
|
526
|
+
|
527
|
+
```ruby
|
528
|
+
class CustomError < StandardError
|
529
|
+
end
|
530
|
+
|
531
|
+
Try(CustomError) {
|
532
|
+
raise CustomError.new
|
533
|
+
} # Try::Failure[T.noreturn](@failure=#<CustomError: CustomError>)
|
534
|
+
|
535
|
+
Try(CustomError) {
|
536
|
+
raise StandardError.new("Not going to be captured")
|
537
|
+
}
|
538
|
+
# > StandardError: Not going to be captured
|
539
|
+
```
|
540
|
+
|
541
|
+
`Try` allows to specify multiple error types:
|
542
|
+
```ruby
|
543
|
+
class CustomError1 < StandardError
|
544
|
+
end
|
545
|
+
|
546
|
+
class CustomError2 < StandardError
|
547
|
+
end
|
548
|
+
|
549
|
+
Try(CustomError1, CustomError2) {
|
550
|
+
raise CustomError1.new
|
551
|
+
} # Try::Failure[T.noreturn](@failure=#<CustomError: CustomError>)
|
552
|
+
|
553
|
+
Try(CustomError1, CustomError2) {
|
554
|
+
raise CustomError2.new
|
555
|
+
} # Try::Failure[T.noreturn](@failure=#<CustomError: CustomError>)
|
556
|
+
|
557
|
+
Try(CustomError1, CustomError2) {
|
558
|
+
raise StandardError.new("Not going to be captured")
|
559
|
+
}
|
560
|
+
# > StandardError: Not going to be captured
|
561
|
+
```
|
562
|
+
|
563
|
+
`Try` can also be initialized directly be invoking constructors of its subtypes:
|
564
|
+
```ruby
|
565
|
+
Mayak::Monads::Try::Success[Integer].new(10) # Try::Success[Integer](@value=10)
|
566
|
+
Mayak::Monads::Try::Failure[Integer].new(StandardError.new) # Try::Failure[Integer](@failure=#<StandardError: StandardError>)
|
567
|
+
```
|
568
|
+
|
569
|
+
### Unpacking
|
570
|
+
|
571
|
+
Since `Try` can either contain success value in `Success` branch or error in `Failure` branch, there are specific accessors
|
572
|
+
for success and failure values. In order to check whether a `Try` contains a successful value or value,
|
573
|
+
predicates `#failure?` and `#success?` can be used:
|
574
|
+
|
575
|
+
```ruby
|
576
|
+
Try { 10 }.success? # true
|
577
|
+
Try { 10 }.failure? # false
|
578
|
+
|
579
|
+
Try { raise "Error" }.success? # false
|
580
|
+
Try { raise "Error" }.failure? # true
|
581
|
+
```
|
582
|
+
|
583
|
+
In order to retrieve a successful value from `Try`, a method `#success` can be used. This method is only defined
|
584
|
+
on `Mayak::Monads::Try::Success`, which is subtype of `Mayak::Monads::Try`. In order to invoke this method without getting an error from sorbet,
|
585
|
+
instance of `Try` should be first coerced to checked and coerced to `Try::Success`. This can be done safely with pattern matching:
|
586
|
+
|
587
|
+
```ruby
|
588
|
+
try = Try { 10 }
|
589
|
+
value = case try
|
590
|
+
when Mayak::Monads::Try::Success
|
591
|
+
try.success
|
592
|
+
else
|
593
|
+
nil
|
594
|
+
end
|
595
|
+
value # 10
|
596
|
+
```
|
597
|
+
|
598
|
+
If an instance of `Try` is a `Failure`, error can be retrieved via `#failure`:
|
599
|
+
|
600
|
+
```ruby
|
601
|
+
try = Try { raise "Error" }
|
602
|
+
value = case try
|
603
|
+
when Mayak::Monads::Try::Failure
|
604
|
+
try.failure
|
605
|
+
when
|
606
|
+
nil
|
607
|
+
end
|
608
|
+
value # #<StandardError: Error>
|
609
|
+
```
|
610
|
+
|
611
|
+
Success and failure values can be retrieved via methods `#success_or` and `#failure_or` as well. These methods
|
612
|
+
receives fallback values:
|
613
|
+
|
614
|
+
```ruby
|
615
|
+
Try { 10 }.success_or(0) # 10
|
616
|
+
Try { 10 }.failure_or(StandardError.new("Error")) # #<StandardError: Error>
|
617
|
+
|
618
|
+
Try { raise "Boom" }.success_or(0) # 0
|
619
|
+
Try { raise "Boom" }.failure_or(StandardError.new("Error")) # #<StandardError: Boom>
|
620
|
+
```
|
621
|
+
|
622
|
+
### Methods
|
623
|
+
|
624
|
+
#### `#to_dry`
|
625
|
+
|
626
|
+
Converts an instance of a monad in to an instance of corresponding dry-monad.
|
627
|
+
|
628
|
+
```ruby
|
629
|
+
include Mayak::Monads::Try::Mixin
|
630
|
+
|
631
|
+
Try { 10 }.to_dry # Try::Value(10)
|
632
|
+
Try { raise "Error" }.to_dry # Try::Error(RuntimeError: Error)
|
633
|
+
```
|
634
|
+
|
635
|
+
|
636
|
+
#### `.from_dry`
|
637
|
+
|
638
|
+
Converts instance of `Dry::Monads::Try` into instance `Mayak::Monads::Try`
|
639
|
+
|
640
|
+
```ruby
|
641
|
+
include Mayak::Monads
|
642
|
+
|
643
|
+
error = StandardError.new("Error")
|
644
|
+
Try.from_dry(Dry::Monads::Try::Value.new([StandardError], 10)) # Try::Success[T.untyped](value = 10)
|
645
|
+
Try.from_dry(Dry::Monads::Try::Error.new(error)) # Try::Failure[T.untyped](error=#<StandardError: Error>)
|
646
|
+
```
|
647
|
+
|
648
|
+
Unfortunately `.from_dry` doesn't preserve types due to the way `Dry::Monads::Try` is written, so
|
649
|
+
this method always returns `Mayak::Monads::Try[T.untyped]`.
|
650
|
+
|
651
|
+
#### `#map`
|
652
|
+
|
653
|
+
The same as `fmap` in a dry-monads `Try`. Allows to modify the value with a block if it's present.
|
654
|
+
|
655
|
+
```ruby
|
656
|
+
sig { returns(Mayak::Monads::Try[Integer]) }
|
657
|
+
def success
|
658
|
+
Mayak::Monads::Try::Success[Integer].new(10)
|
659
|
+
end
|
660
|
+
|
661
|
+
sig { returns(Mayak::Monads::Try[Integer]) }
|
662
|
+
def failure
|
663
|
+
Mayak::Monads::Try::Failure[Integer].new(StandardError.new("Error"))
|
664
|
+
end
|
665
|
+
|
666
|
+
success.map { |a| a + 20 } # Success[Integer](value = 20)
|
667
|
+
failure.map { |a| a + 20 } # Failure[Integer](error=#<StandardError: Error>)
|
668
|
+
```
|
669
|
+
|
670
|
+
#### `#flat_map`
|
671
|
+
The same as `bind` in a dry-monads `Try`. Allows to modify the value with a block that returns another `Try`
|
672
|
+
if it's a`Success`, otherwise returns `Failure`. If the block returns `Failure`, the whole computation returns `Failure`.
|
673
|
+
|
674
|
+
```ruby
|
675
|
+
sig { params(a: Integer, b: Integer).returns(Mayak::Monads::Try[Integer]) }
|
676
|
+
def divide(a, b)
|
677
|
+
Try { a / b }
|
678
|
+
end
|
679
|
+
|
680
|
+
divide(20, 2).flat_map { |a| divide(a, 2) } # Try::Success[Integer](value = 5)
|
681
|
+
divide(20, 2).flat_map { |a| divide(a, 0) } # Try::Failure[Integer](@failure=#<ZeroDivisionError: divided by 0>)
|
682
|
+
divide(20, 0).flat_map { |a| divide(a, 2) } # Try::Failure[Integer](@failure=#<ZeroDivisionError: divided by 0>)
|
683
|
+
```
|
684
|
+
|
685
|
+
#### `#filter_or`
|
686
|
+
|
687
|
+
Receives a block the returns a boolean value and checks the underlying value with the block when a monad is `Success`.
|
688
|
+
Returns `Failure` with provided error if the block called on the value returns `false`, and returns `self` if the block returns `true`.
|
689
|
+
Returns self if the original monad is `Failure`.
|
690
|
+
|
691
|
+
```ruby
|
692
|
+
error = StandardError.new("Provided error")
|
693
|
+
divide(20, 2).filter_or(error) { |value| value > 5 } # Try::Success[Integer](value = 5)
|
694
|
+
divide(20, 2).filter_or(error) { |value| value < 5 } # Try::Failure[Integer](@failure=#<StandardError: Provided error>)
|
695
|
+
divide(20, 0).filter_or(error) { |value| value < 5 } # Try::Failure[Integer](@failure=#<ZeroDivisionError: divided by 0>)
|
696
|
+
```
|
697
|
+
|
698
|
+
#### `#success?`
|
699
|
+
|
700
|
+
Returns true if a `Try` is a `Success` and false if it's a `Failure`.
|
701
|
+
|
702
|
+
```ruby
|
703
|
+
divide(20, 2).success? # true
|
704
|
+
divide(20, 0).success? # false
|
705
|
+
```
|
706
|
+
|
707
|
+
#### `#failure?`
|
708
|
+
|
709
|
+
Returns true if a `Try` is a `Failure` and false if it's a `Success`.
|
710
|
+
|
711
|
+
```ruby
|
712
|
+
divide(20, 2).failure? # false
|
713
|
+
divide(20, 0).failure? # true
|
714
|
+
```
|
715
|
+
|
716
|
+
#### `#success_or`
|
717
|
+
|
718
|
+
Unpack a `Try` and returns its success value if it's a `Success`, or returns provided fallback value if it's a `Failure`.
|
719
|
+
|
720
|
+
```ruby
|
721
|
+
divide(20, 2).value_or(0) # 10
|
722
|
+
divide(20, 0).value_or(0) # 0
|
723
|
+
```
|
724
|
+
|
725
|
+
#### `#failure_or`
|
726
|
+
|
727
|
+
Unpack a `Try` and returns its error if it's a `Failure`,
|
728
|
+
or returns provided fallback error of `StandardError` subtype if it's a `Success`.
|
729
|
+
|
730
|
+
```ruby
|
731
|
+
divide(20, 2).failure_or(StandardError.new("Error")) # #<StandardError: Error>
|
732
|
+
divide(20, 0).failure_or(StandardError.new("Error")) # #<ZeroDivisionError: divided by 0>
|
733
|
+
```
|
734
|
+
|
735
|
+
#### `#either`
|
736
|
+
|
737
|
+
Receives two functions: one from an error to a result
|
738
|
+
value (failure function), and another one from successful value to a result value (success function).
|
739
|
+
If a `Try` is an instance of `Success`, applies success function to a success value,
|
740
|
+
otherwise applies error function to an error. Note that both functions must have the same return type.
|
741
|
+
|
742
|
+
```ruby
|
743
|
+
divide(10, 2).either(
|
744
|
+
-> (error) { error.message },
|
745
|
+
-> (value) { value.to_s }
|
746
|
+
) # 5
|
747
|
+
|
748
|
+
divide(10, 0).either(
|
749
|
+
-> (error) { error.message },
|
750
|
+
-> (value) { value.to_s }
|
751
|
+
) # Division by zero
|
752
|
+
```
|
753
|
+
|
754
|
+
#### `#tee`
|
755
|
+
|
756
|
+
If a `Try` is an instance of `Success`, runs a block with a value of `Success` passed, and returns the monad itself
|
757
|
+
unchanged. Doesn't do anything if the monad is an instance of `Failure`.
|
758
|
+
|
759
|
+
```ruby
|
760
|
+
divide(10, 2).tee { |a| puts a }
|
761
|
+
# returns: Try::Success[Integer](value = 5)
|
762
|
+
# console: 5
|
763
|
+
|
764
|
+
divide(10, 0).tee { |a| puts a }
|
765
|
+
# returns: Try::Failure[Integer](@failure=#<ZeroDivisionError: divided by 0>)
|
766
|
+
```
|
767
|
+
|
768
|
+
Can be useful to embed side-effects into chain of monad transformation:
|
769
|
+
```ruby
|
770
|
+
sig { params(a: Integer, b: Integer).returns(Try[String]) }
|
771
|
+
def run(a, b)
|
772
|
+
divide(a, b)
|
773
|
+
.tee { |value| logger.info("#{a} / #{b} = #{value}") }
|
774
|
+
.map { |value| value * 100 }
|
775
|
+
.tee { |value| logger.info("Intermediate result = #{value}") }
|
776
|
+
.map(&:to_s)
|
777
|
+
end
|
778
|
+
```
|
779
|
+
|
780
|
+
#### `#map_failure`
|
781
|
+
|
782
|
+
Transforms an error with a block if a monad is `Failure`. Returns itself otherwise. Note that the passed block
|
783
|
+
should return an instance of `StandardError`, or an instance of a subtype of the `StandardError`.
|
784
|
+
|
785
|
+
```ruby
|
786
|
+
divide(10, 0).map_failure { |error| CustomError.new(message) } # Try::Failure[Integer](@failure=#<CustomError: Division by zero>)
|
787
|
+
divide(10, 2).map_failure { |error| CustomError.new(message) } # Try::Success[Integer](@value=5)
|
788
|
+
```
|
789
|
+
|
790
|
+
#### `#flat_map_failure`
|
791
|
+
|
792
|
+
Transforms an error with a block that returns an instance of `Try` if a monad is `Failure`. Returns itself otherwise.
|
793
|
+
Allows to recover from error with a computation, that can fail too.
|
794
|
+
|
795
|
+
```ruby
|
796
|
+
divide(10, 2).flat_map_failure { |_error|
|
797
|
+
Mayak::Monads::Try::Success[Integer].new(10)
|
798
|
+
} # Try::Success[Integer](@value=5)
|
799
|
+
|
800
|
+
divide(10, 2).flat_map_failure { |error|
|
801
|
+
Mayak::Monads::Try::Failure[Integer].new(StandardError.new(error.message))
|
802
|
+
} # Try::Success[Integer](@value=5)
|
803
|
+
|
804
|
+
divide(10, 0).flat_map_failure { |error|
|
805
|
+
Mayak::Monads::Try::Success[Integer].new(10)
|
806
|
+
} # Try::Success[Integer](@value=2)
|
807
|
+
|
808
|
+
divide(10, 0).flat_map_failure { |error|
|
809
|
+
Mayak::Monads::Try::Failure[Integer].new(CustomError.new(error.message))
|
810
|
+
} # Try::Failure[Integer](@failure=#<CustomError: Division by zero>)
|
811
|
+
```
|
812
|
+
|
813
|
+
#### `#to_result`
|
814
|
+
|
815
|
+
Converts a `Try` into a `Result`. If the `Try` is a `Try::Success`, returns `Result::Success` with a success value.
|
816
|
+
If it's a `Failure`, returns `Result::Failre` with a an error value.
|
817
|
+
|
818
|
+
```ruby
|
819
|
+
divide(10, 2).to_result # Result::Success[StandardError, Integer](value = 5)
|
820
|
+
divide(10, 0).to_result # Result::Failure[StandardError, Integer](error = #<ZeroDivisionError: divided by 0>)
|
821
|
+
```
|
822
|
+
|
823
|
+
#### `#to_task`
|
824
|
+
|
825
|
+
Converts a value to task. If a `Try` is an instance of `Success`, it returns succeeded task, if it's a `Failure`,
|
826
|
+
it returns failed task with an `Failure`'s error.
|
827
|
+
|
828
|
+
```ruby
|
829
|
+
task = Mayak::Concurrent::Task.execute { 100 }
|
830
|
+
task.flat_map { |value| divide(value, 10).to_task }.await! # 10
|
831
|
+
task.flat_map { |value| divide(value, 0).to_task }.await! # StandardError: Divison by zero
|
832
|
+
```
|
833
|
+
|
834
|
+
#### `#to_maybe`
|
835
|
+
|
836
|
+
Converts a `Try` value to `Maybe`. When the `Try` instance is a `Success`, returns `Maybe` containing successful value.
|
837
|
+
When the `Try` is an instance of `Failure`, returns `None`. Note that during the transformation error is lost.
|
838
|
+
|
839
|
+
```ruby
|
840
|
+
divide(10, 2).to_maybe # Some[Integer](value = 5)
|
841
|
+
divide(10, 0).to_maybe # None[Integer]
|
842
|
+
```
|
843
|
+
|
844
|
+
#### `#as`
|
845
|
+
|
846
|
+
Substitutes successful value in `Try::Success` with a new value, if the monad is instance of `Try::Failure`, returns
|
847
|
+
the monad unchanged.
|
848
|
+
|
849
|
+
```ruby
|
850
|
+
divide(10, 2).as("Success")# Try::Success[String](@value="Success")
|
851
|
+
divide(10, 0).as("Success") # Try::Failure[String](@failure=#<ZeroDivisionError: Division by zero>)
|
852
|
+
```
|
853
|
+
|
854
|
+
#### `#failure_as`
|
855
|
+
|
856
|
+
Substitutes an error `Try::Failure` with a new error, if the monad is instance of `Try::Success`, returns
|
857
|
+
the monad unchanged.
|
858
|
+
|
859
|
+
```ruby
|
860
|
+
divide(10, 2).failure_as(StandardError.new("Error")) # Try::Success[Integer](@value=5)
|
861
|
+
divide(10, 0).failure_as(StandardError.new("Error")) # Try::Failure[Integer](@failure=#<StandardError: Error>)
|
862
|
+
```
|
863
|
+
|
864
|
+
#### `#recover`
|
865
|
+
|
866
|
+
Converts `Failure` into a `Success` with a provided value. If the monad is an instance of `Success`, returns itself.
|
867
|
+
Basically, recovers from an error with a successful value.
|
868
|
+
|
869
|
+
```ruby
|
870
|
+
divide(10, 2).recover(0) # Try::Success[Integer](value = 5)
|
871
|
+
divide(10, 0).recover(0) # Try::Success[Integer](value = 5)
|
872
|
+
```
|
873
|
+
|
874
|
+
#### `#recover_on`
|
875
|
+
|
876
|
+
Converts `Failure` into a `Success` with a provided value if an error is an instance of a subtype of provided class. If the monad is an instance of `Success`, returns itself.
|
877
|
+
Allows to recover from specific errors.
|
878
|
+
|
879
|
+
```ruby
|
880
|
+
class CustomError1 < StandardError
|
881
|
+
end
|
882
|
+
|
883
|
+
class CustomError2 < StandardError
|
884
|
+
end
|
885
|
+
|
886
|
+
failure1 = Mayak::Monads::Try::Failure[String].new(CustomError1.new("Error1"))
|
887
|
+
failure2 = Mayak::Monads::Try::Failure[String].new(CustomError2.new("Error2"))
|
888
|
+
|
889
|
+
failure1.recover_on(CustomError1) { |error| error.message } # Try::Success[String](@value="Error1")
|
890
|
+
failure2.recover_on(CustomError1) { |error| error.message } # Try::Failure[String](@failure=#<CustomError2: Error2>)
|
891
|
+
```
|
892
|
+
|
893
|
+
Note, that an error passed in the block is not down casted. So from sorbet perspective it still will be
|
894
|
+
a `StandardError`.
|
895
|
+
|
896
|
+
```ruby
|
897
|
+
failure1.recover_on(CustomError1) { |error|
|
898
|
+
T.reveal_type(error)
|
899
|
+
"Error"
|
900
|
+
}
|
901
|
+
# Revealed type: StandardError
|
902
|
+
```
|
903
|
+
|
904
|
+
#### `#recover_with`
|
905
|
+
|
906
|
+
Alias for `#flat_map_failure`.
|
907
|
+
|
908
|
+
#### `.sequence`
|
909
|
+
`Try.sequence` takes an array of `Try`s and transform it into a `Try` of an array.
|
910
|
+
If all elements of the array is `Success`, then the method returns `Try::Sucess` containing array of values,
|
911
|
+
otherwise the method returns `Try::Failure` containing first error.
|
912
|
+
|
913
|
+
```ruby
|
914
|
+
include Mayak::Monads
|
915
|
+
|
916
|
+
values = [
|
917
|
+
Try::Success[Integer].new(1),
|
918
|
+
Try::Success[Integer].new(2),
|
919
|
+
Try::Success[Integer].new(3)
|
920
|
+
]
|
921
|
+
Try.sequence(values) # Try::Success[T::Array[Integer]](@value=[1, 2, 3])
|
922
|
+
|
923
|
+
values = [
|
924
|
+
Try::Success[Integer].new(1),
|
925
|
+
Try::Failure[Integer].new(ArgumentError.new("Error1")),
|
926
|
+
Try::Failure[Integer].new(StandardError.new("Error2"))
|
927
|
+
]
|
928
|
+
Try.sequence(values) # Try::Failure[Integer](@failure=#<ArgumentError: Error1>)
|
929
|
+
```
|
930
|
+
|
931
|
+
#### `.check`
|
932
|
+
|
933
|
+
Receives a value, an error, and block returning a boolean value. If the block returns true,
|
934
|
+
the method returns the value wrapped in `Try::Success`, otherwise it returns `Try::Failure` containing the error:
|
935
|
+
|
936
|
+
```ruby
|
937
|
+
error = StandardError.new("Error")
|
938
|
+
Try.check(10, error) { 20 > 10 } # Try::Success[Integer](@failure=10)
|
939
|
+
Try.check(20, error) { 10 > 20 } # Try::Failure[Integer](@failure=#<StandardError: Error>)
|
940
|
+
```
|
941
|
+
|
942
|
+
#### `.guard`
|
943
|
+
|
944
|
+
Receives a block returning a boolean value, and an error. If the block returns true,
|
945
|
+
the method returns `Try::Success` containing nil, otherwise it returns `Try::Failure` containing an error:
|
946
|
+
|
947
|
+
```ruby
|
948
|
+
error = StandardError.new("Error")
|
949
|
+
Maybe.guard(error) { 20 > 10 } # Try::Success[NilClass](@failure=nil)
|
950
|
+
Maybe.guard(error) { 10 > 20 } # Try::Failure[NilClass](@failure=#<StandardError: Error>)
|
951
|
+
```
|
952
|
+
|
953
|
+
This method may be useful in do-notations when you need need to check some invariant and perform
|
954
|
+
an early return if it doesn't hold.
|
955
|
+
|
956
|
+
### Do-notation
|
957
|
+
|
958
|
+
`Try` monad supports `do-notation` just like `Maybe` monad. Check do-notation chapter of [Maybe](#maybe) for motivation.
|
959
|
+
Do-notation for `Try` is quite similar for `Maybe`'s do-notation, albeit there are some differences in syntax and semantics.
|
960
|
+
Do-notation scope is created via method `for_try`, a monad is unwrapped via `do_try!`.
|
961
|
+
|
962
|
+
```ruby
|
963
|
+
result = for_try {
|
964
|
+
first = do_try! Try::Success[Integer].new(10)
|
965
|
+
second = do_try! Try::Success[Integer].new(20)
|
966
|
+
first + second
|
967
|
+
}
|
968
|
+
result # Try::Success[Integer](@value=30)
|
969
|
+
```
|
970
|
+
|
971
|
+
When an instance of `Try::Failure` is unwrapped via `do_try!` the whole computation returns this monad.
|
972
|
+
|
973
|
+
```ruby
|
974
|
+
result = for_try {
|
975
|
+
first = do_try! Try::Success[Integer].new(10)
|
976
|
+
second = do_try! Try::Failure[Integer].new(StandardError.new("Error"))
|
977
|
+
third = do_try! Try::Success[Integer].new(20)
|
978
|
+
first + second + third
|
979
|
+
}
|
980
|
+
result # Try::Failure[Integer](@failure=#<StandardError: Error>)
|
981
|
+
```
|
982
|
+
|
983
|
+
Methods `check_try!` and `guard_try!` to perform early returns.
|
984
|
+
|
985
|
+
```ruby
|
986
|
+
sig { params(a: Integer, b: Integer).returns(Try[Float]) }
|
987
|
+
def compute(a, b)
|
988
|
+
argument_error = ArgumentError.new("Argument is less than zero")
|
989
|
+
for_try {
|
990
|
+
guard_try! (argument_error) { a < 0 || b < 0 }
|
991
|
+
first = do_try! Try { a / b }
|
992
|
+
second = do_try! Try { Math.log(a, b) }
|
993
|
+
result = first + second
|
994
|
+
check_try!(result, StandardError.new("Number is too big")) { result < 100 }
|
995
|
+
}
|
996
|
+
end
|
997
|
+
```
|
998
|
+
|
999
|
+
Do-notation for `Try` is fully typed as well.
|
1000
|
+
|
1001
|
+
```ruby
|
1002
|
+
for_try {
|
1003
|
+
value = do_try! Try { 10 }
|
1004
|
+
T.reveal_type(value)
|
1005
|
+
value
|
1006
|
+
}
|
1007
|
+
# > Revealed type: Integer
|
1008
|
+
|
1009
|
+
result = for_try {
|
1010
|
+
value = do_try! Try { 10 }
|
1011
|
+
value
|
1012
|
+
}
|
1013
|
+
T.reveal_type(result)
|
1014
|
+
# > Revealed type: Integer
|
1015
|
+
```
|
1016
|
+
|
1017
|
+
## Result
|
1018
|
+
|
1019
|
+
`Result` monad represents result of a computation that can succeed with an arbitrary value, or fail with an arbitrary error.
|
1020
|
+
As `Try`, `Result` has two subtypes: `Failure` which contains an error and represents failure case, and `Success`, which contains
|
1021
|
+
a success value. The difference between `Result` and `Try`, is that `Result` has arbitrary error type, while `Try` has it fixed to `StandardError`.
|
1022
|
+
|
1023
|
+
|
1024
|
+
### Initialization
|
1025
|
+
|
1026
|
+
The primary way to create an instance of `Try` is to use constructor method `#Try` from `Mayak::Monads::Try::Mixin`.
|
1027
|
+
This method receives a block that may raise exceptions. If an exception has been raised inside the block,
|
1028
|
+
the method will return instance of `Try::Failure` containing an error. Otherwise the method will return result of the block
|
1029
|
+
wrapped into `Try::Success`
|
1030
|
+
|
1031
|
+
`Result` can be created via primary constructors of `Result::Success` and `Result::Failure`:
|
1032
|
+
|
1033
|
+
```ruby
|
1034
|
+
include Mayak::Monads
|
1035
|
+
|
1036
|
+
Result::Success[String, Integer].new(10)
|
1037
|
+
Result::Failure[String, Integer].new("Error")
|
1038
|
+
```
|
1039
|
+
|
1040
|
+
### Unpacking
|
1041
|
+
|
1042
|
+
Accessing values of `Result` performed in the same as for `Try`.
|
1043
|
+
|
1044
|
+
```ruby
|
1045
|
+
success = Result::Success[String, Integer].new(10)
|
1046
|
+
failure = Result::Failure[String, Integer].new("Error")
|
1047
|
+
success.success? # true
|
1048
|
+
success.failure? # false
|
1049
|
+
|
1050
|
+
failure.success? # false
|
1051
|
+
failure.failure? # true
|
1052
|
+
```
|
1053
|
+
|
1054
|
+
Successful and failure values can be accessed via `#success` and `#failure` values. These methods
|
1055
|
+
are defined on `Result::Success`, and `Result::Failure` subtypes respectively, so in order to access
|
1056
|
+
them value of type `Result` should be downcasted first:
|
1057
|
+
|
1058
|
+
```ruby
|
1059
|
+
result = Result::Success[String, Integer].new(10)
|
1060
|
+
value = case result
|
1061
|
+
when Result::Success
|
1062
|
+
try.success
|
1063
|
+
else
|
1064
|
+
nil
|
1065
|
+
end
|
1066
|
+
value # 10
|
1067
|
+
|
1068
|
+
result = Result::Failure[String, Integer].new("Error")
|
1069
|
+
value = case result
|
1070
|
+
when Result::Failure
|
1071
|
+
result.failure
|
1072
|
+
when
|
1073
|
+
nil
|
1074
|
+
end
|
1075
|
+
value # "Error"
|
1076
|
+
```
|
1077
|
+
|
1078
|
+
Success and failure values can be retrieved via methods `#success_or` and `#failure_or` as well. These methods
|
1079
|
+
receives fallback values:
|
1080
|
+
|
1081
|
+
```ruby
|
1082
|
+
Result::Success[String, Integer].new(10).success_or(0) # 10
|
1083
|
+
Result::Success[String, Integer].new(10).failure_or("Error") # "Error"
|
1084
|
+
|
1085
|
+
Result::Failure[String, Integer].new("Error").success_or(0) # 0
|
1086
|
+
Result::Failure[String, Integer].new("Error").failure_or("Another Error") # "Error"
|
1087
|
+
```
|
1088
|
+
|
1089
|
+
### Methods
|
1090
|
+
|
1091
|
+
#### `#to_dry`
|
1092
|
+
|
1093
|
+
Converts an instance of a monad in to an instance of corresponding dry-monad.
|
1094
|
+
|
1095
|
+
```ruby
|
1096
|
+
Result::Success[String, Integer].new(10).to_dry # Success(10): Dry::Monads::Result::Success
|
1097
|
+
Result::Failure[String, Integer].new("Error").to_dry # Failure("Error"): Dry::Monads::Result::Failure
|
1098
|
+
```
|
1099
|
+
|
1100
|
+
|
1101
|
+
#### `.from_dry`
|
1102
|
+
|
1103
|
+
Converts instance of `Dry::Monads::Result` into instance `Mayak::Monads::Result`
|
1104
|
+
|
1105
|
+
```ruby
|
1106
|
+
sig { returns(Dry::Monads::Result::Success[String, Integer]) }
|
1107
|
+
def dry_success
|
1108
|
+
Dry::Monads::Result::Success.new(10)
|
1109
|
+
end
|
1110
|
+
|
1111
|
+
sig { returns(Dry::Monads::Result::Success[String, Integer]) }
|
1112
|
+
def dry_failure
|
1113
|
+
Dry::Monads::Result::Failure.new("Error")
|
1114
|
+
end
|
1115
|
+
|
1116
|
+
Try.from_dry(dry_success) # Mayak::Monads::Result::Success[String, Integer](value = 10)
|
1117
|
+
Try.from_dry(dry_failure) # Mayak::Monads::Result::Success[String, Integer]("Error")
|
1118
|
+
```
|
1119
|
+
|
1120
|
+
Unfortunately `.from_dry` doesn't preserve types due to the way `Dry::Monads::Try` is written, so
|
1121
|
+
this method always returns `Mayak::Monads::Try[T.untyped]`.
|
1122
|
+
|
1123
|
+
#### `#map`
|
1124
|
+
|
1125
|
+
The same as `fmap` in a dry-monads `Result`. Allows to modify the value with a block if it's present.
|
1126
|
+
|
1127
|
+
```ruby
|
1128
|
+
sig { returns(Result[String, Integer]) }
|
1129
|
+
def success
|
1130
|
+
Result::Success[String, Integer].new(10)
|
1131
|
+
end
|
1132
|
+
|
1133
|
+
sig { returns(Result[String, Integer]) }
|
1134
|
+
def failure
|
1135
|
+
Result::Failure[String, Integer].new("Error")
|
1136
|
+
end
|
1137
|
+
|
1138
|
+
success.map { |a| a + 20 } # Result::Success[Integer](value = 20)
|
1139
|
+
failure.map { |a| a + 20 } # Result::Failure[Integer](error=#<StandardError: Error>)
|
1140
|
+
```
|
1141
|
+
|
1142
|
+
#### `#flat_map`
|
1143
|
+
|
1144
|
+
The same as `bind` in a dry-monads `Result`. Allows to modify the value with a block that returns another `Result`
|
1145
|
+
if it's a `Result::Success`, otherwise returns `Result::Failure`. If the block returns `Result::Failure`, the whole computation returns `Result::Failure`.
|
1146
|
+
|
1147
|
+
```ruby
|
1148
|
+
sig { params(a: Integer, b: Integer).returns(Mayak::Monads::Result[String, Integer]) }
|
1149
|
+
def divide(a, b)
|
1150
|
+
if a == 0
|
1151
|
+
Result::Failure[String, Integer].new("Division by zero")
|
1152
|
+
else
|
1153
|
+
Result::Success[String, Integer].new(a / b)
|
1154
|
+
end
|
1155
|
+
end
|
1156
|
+
|
1157
|
+
divide(20, 2).flat_map { |a| divide(a, 2) } # Result::Success[String, Integer](value = 5)
|
1158
|
+
divide(20, 2).flat_map { |a| divide(a, 0) } # Result::Failure[String, Integer](@failure="Division by zero")
|
1159
|
+
divide(20, 0).flat_map { |a| divide(a, 2) } # Result::Failure[String, Integer](@failure="Division by zero")
|
1160
|
+
```
|
1161
|
+
|
1162
|
+
#### `#filter_or`
|
1163
|
+
|
1164
|
+
Receives a block the returns a boolean value and checks the underlying value with the block when a monad is `Result::Success`.
|
1165
|
+
Returns `Result::Failure` with provided error if the block called on the value returns `false`, and returns `self` if the block returns `true`.
|
1166
|
+
Returns self if the original monad is `Result::Failure`.
|
1167
|
+
|
1168
|
+
```ruby
|
1169
|
+
divide(10, 2).filter_or("Above 5") { |value| value <= 5 } # Result::Success[String, Integer](value = 5)
|
1170
|
+
divide(20, 2).filter_or("Above 5") { |value| value <= 5 } # Result::Failure[String, Integer](@failure="Above 5")
|
1171
|
+
divide(10, 0).filter_or("Above 5") { |value| value <= 5 } # Result::Failure[String, Integer](@failure="Division by zero")
|
1172
|
+
```
|
1173
|
+
|
1174
|
+
#### `#success?`
|
1175
|
+
|
1176
|
+
Returns true if a `Result` is a `Result::Success` and false if it's a `Result::Failure`.
|
1177
|
+
|
1178
|
+
```ruby
|
1179
|
+
divide(20, 2).success? # true
|
1180
|
+
divide(20, 0).success? # false
|
1181
|
+
```
|
1182
|
+
|
1183
|
+
#### `#failure?`
|
1184
|
+
|
1185
|
+
Returns true if a `Result` is a `Result::Failure` and false if it's a `Result::Success`.
|
1186
|
+
|
1187
|
+
```ruby
|
1188
|
+
divide(20, 2).failure? # false
|
1189
|
+
divide(20, 0).failure? # true
|
1190
|
+
```
|
1191
|
+
|
1192
|
+
#### `#success_or`
|
1193
|
+
|
1194
|
+
Unpack a `Result` and returns its success value if it's a `Result::Success`, or returns provided fallback value if it's a `Result::Failure`.
|
1195
|
+
|
1196
|
+
```ruby
|
1197
|
+
divide(20, 2).value_or(0) # 10
|
1198
|
+
divide(20, 0).value_or(0) # 0
|
1199
|
+
```
|
1200
|
+
|
1201
|
+
#### `#failure_or`
|
1202
|
+
|
1203
|
+
Unpack a `Result` and returns its error if it's a `Result::Failure`,
|
1204
|
+
or returns provided fallback error of `StandardError` subtype if it's a `Success`.
|
1205
|
+
|
1206
|
+
```ruby
|
1207
|
+
divide(20, 2).failure_or("Error") # "Error"
|
1208
|
+
divide(20, 0).failure_or("Error") # "Division by zero"
|
1209
|
+
```
|
1210
|
+
|
1211
|
+
#### `#flip`
|
1212
|
+
|
1213
|
+
Flips failure and success channels, converting `Result[A, B]` into `Result[B, A]`.
|
1214
|
+
Basically, transforms `Success` into `Failure` and vice versa.
|
1215
|
+
|
1216
|
+
|
1217
|
+
```ruby
|
1218
|
+
divide(10, 2).flip # Result::Failure[Integer, String](value = 5)
|
1219
|
+
divide(10, 0).flip # Result::Success[Integer, String](value = "Division by zero")
|
1220
|
+
```
|
1221
|
+
|
1222
|
+
#### `#either`
|
1223
|
+
|
1224
|
+
Receives two functions: one from an error to a result
|
1225
|
+
value (failure function), and another one from successful value to a result value (success function).
|
1226
|
+
If a `Result` is an instance of `Result::Success`, applies success function to a success value,
|
1227
|
+
otherwise applies error function to an error. Note that both functions must have the same return type.
|
1228
|
+
|
1229
|
+
```ruby
|
1230
|
+
divide(10, 2).either(
|
1231
|
+
-> (error) { "Error occurred: `#{error}`" },
|
1232
|
+
-> (value) { value.to_s }
|
1233
|
+
) # 5
|
1234
|
+
|
1235
|
+
divide(10, 0).either(
|
1236
|
+
-> (error) { "Error occurred: #{error}" },
|
1237
|
+
-> (value) { value.to_s }
|
1238
|
+
) # Error occurred: `Division by zero`
|
1239
|
+
```
|
1240
|
+
|
1241
|
+
#### `#tee`
|
1242
|
+
|
1243
|
+
If a `Result` is an instance of `Result::Success`, runs a block with a value of `Result::Success` passed, and returns the monad itself
|
1244
|
+
unchanged. Doesn't do anything if the monad is an instance of `Result::Failure`.
|
1245
|
+
|
1246
|
+
```ruby
|
1247
|
+
divide(10, 2).tee { |a| puts a }
|
1248
|
+
# returns: Result::Success[String, Integer](value=5)
|
1249
|
+
# console: 5
|
1250
|
+
|
1251
|
+
divide(10, 0).tee { |a| puts a }
|
1252
|
+
# returns: Result::Failure[String, Integer](@failure="Division by zero")
|
1253
|
+
```
|
1254
|
+
|
1255
|
+
#### `#map_failure`
|
1256
|
+
|
1257
|
+
Transforms an error with a block if a monad is `Failure`. Returns itself otherwise. Note that the passed block
|
1258
|
+
should return an instance of `StandardError`, or an instance of a subtype of the `StandardError`.
|
1259
|
+
|
1260
|
+
```ruby
|
1261
|
+
divide(10, 0).map_failure { |error| "Error occurred: `#{error}`" } # Result::Failure[String, Integer](@failure="Error occurred: `Division by zero`")
|
1262
|
+
divide(10, 2).map_failure { |error| "Error occurred: `#{error}`" } # Result::Success[String, Integer](@value=5)
|
1263
|
+
```
|
1264
|
+
|
1265
|
+
#### `#flat_map_failure`
|
1266
|
+
|
1267
|
+
Transforms an error with a block that returns an instance of `Result` if a monad is `Result::Failure`. Returns itself otherwise.
|
1268
|
+
Allows to recover from error with a computation, that can fail too.
|
1269
|
+
|
1270
|
+
```ruby
|
1271
|
+
divide(10, 2).flat_map_failure { |_error|
|
1272
|
+
Result::Success[String, Integer].new(0)
|
1273
|
+
} # Result::Success[String, Integer](@value=5)
|
1274
|
+
|
1275
|
+
divide(10, 2).flat_map_failure { |error|
|
1276
|
+
Result::Failure[String, Integer].new("Error")
|
1277
|
+
} # Result::Success[String, Integer](@value=2)
|
1278
|
+
|
1279
|
+
divide(10, 0).flat_map_failure { |error|
|
1280
|
+
Result::Success[String, Integer].new(0)
|
1281
|
+
} # Result::Success[String, Integer](@value=0)
|
1282
|
+
|
1283
|
+
divide(10, 0).flat_map_failure { |error|
|
1284
|
+
Result::Success[String, Integer].new("Error")
|
1285
|
+
} # Result::Failure[String, Integer](@failure="Error")
|
1286
|
+
```
|
1287
|
+
|
1288
|
+
#### `#to_task`
|
1289
|
+
|
1290
|
+
Converts a value to task. Receives a block that converts an error into an instance of `StandardError`.
|
1291
|
+
If a `Result` is an instance of `Result::Success`, it returns succeeded task.
|
1292
|
+
If it's a `Result::Failure`, it returns failed task with an `Failure`'s error returned by the block.
|
1293
|
+
|
1294
|
+
```ruby
|
1295
|
+
task = Mayak::Concurrent::Task.execute { 100 }
|
1296
|
+
|
1297
|
+
task.flat_map { |value|
|
1298
|
+
divide(value, 10).to_task { |error| StandardError.new(error) }
|
1299
|
+
}.await! # 10
|
1300
|
+
|
1301
|
+
task.flat_map { |value|
|
1302
|
+
divide(value, 0).to_task { |error| StandardError.new(error) }
|
1303
|
+
}.await! # StandardError: Divison by zero
|
1304
|
+
```
|
1305
|
+
|
1306
|
+
#### `#to_try`
|
1307
|
+
|
1308
|
+
Converts a `Result` into a `Try`. Receives a block that converts an error into an instance of `StandardError`.
|
1309
|
+
If the `Result` is a `Result::Success`, returns `Try::Success` with a success value.
|
1310
|
+
If it's a `Result::Failure`, returns `Try::Failre` with a an error value returned by the block.
|
1311
|
+
|
1312
|
+
```ruby
|
1313
|
+
divide(10, 2).to_result { |error| StandardError.new(error) } # Try::Success[Integer](value = 5)
|
1314
|
+
divide(10, 0).to_result { |error| StandardError.new(error) } # Try::Failure[Integer](error = #<ZeroDivisionError: divided by 0>)
|
1315
|
+
```
|
1316
|
+
|
1317
|
+
#### `#to_maybe`
|
1318
|
+
|
1319
|
+
Converts a `Result` value to `Maybe`. When the `Result` instance is a `Result::Success`, returns `Maybe` containing successful value.
|
1320
|
+
When the `Result` is an instance of `Result::Failure`, returns `None`. Note that during the transformation error is lost.
|
1321
|
+
|
1322
|
+
```ruby
|
1323
|
+
divide(10, 2).to_maybe # Some[Integer](value = 5)
|
1324
|
+
divide(10, 0).to_maybe # None[Integer]
|
1325
|
+
```
|
1326
|
+
|
1327
|
+
#### `#as`
|
1328
|
+
|
1329
|
+
Substitutes successful value in `Result::Success` with a new value, if the monad is instance of `Result::Failure`, returns
|
1330
|
+
the monad unchanged.
|
1331
|
+
|
1332
|
+
```ruby
|
1333
|
+
divide(10, 2).as("Success") # Result::Success[String, String](@value="Success")
|
1334
|
+
divide(10, 0).as("Success") # Result::Failure[String, String](@failure="Division by zero")
|
1335
|
+
```
|
1336
|
+
|
1337
|
+
#### `#failure_as`
|
1338
|
+
|
1339
|
+
Substitutes an error `Result::Failure` with a new error, if the monad is instance of `Try::Success`, returns
|
1340
|
+
the monad unchanged.
|
1341
|
+
|
1342
|
+
```ruby
|
1343
|
+
divide(10, 2).failure_as(0) # Result::Success[Integer, Integer](@value=5)
|
1344
|
+
divide(10, 0).failure_as(0) # Result::Failure[Integer, Integer](@failure=0)
|
1345
|
+
```
|
1346
|
+
|
1347
|
+
#### `#recover`
|
1348
|
+
|
1349
|
+
Converts `Result::Failure` into a `Result::Success` with a provided value. If the monad is an instance of `Result::Success`, returns itself.
|
1350
|
+
Basically, recovers from an error with a successful value.
|
1351
|
+
|
1352
|
+
```ruby
|
1353
|
+
divide(10, 2).recover(0) # Result::Success[String, Integer](value = 5)
|
1354
|
+
divide(10, 0).recover(0) # Result::Success[Integer](value = 5)
|
1355
|
+
```
|
1356
|
+
|
1357
|
+
#### `#recover_with`
|
1358
|
+
|
1359
|
+
Receives a block that converts an error into successful value, and converts `Failure` into a `Success` with the value.
|
1360
|
+
If the monad is an instance of `Success`, returns itself.Basically, recovers from an error with a successful value.
|
1361
|
+
|
1362
|
+
```ruby
|
1363
|
+
divide(10, 2).recover_with { |error|
|
1364
|
+
if error == "Division by zero"
|
1365
|
+
0
|
1366
|
+
else
|
1367
|
+
-1
|
1368
|
+
end
|
1369
|
+
} # Result::Success[String, Integer](value = 5)
|
1370
|
+
divide(10, 2).recover_with { |error|
|
1371
|
+
if error == "Division by zero"
|
1372
|
+
0
|
1373
|
+
else
|
1374
|
+
-1
|
1375
|
+
end
|
1376
|
+
} # Result::Success[Integer](value = 0)
|
1377
|
+
```
|
1378
|
+
|
1379
|
+
#### `#recover_with_result`
|
1380
|
+
|
1381
|
+
Alias for `#flat_map_failure`.
|
1382
|
+
|
1383
|
+
#### `.sequence`
|
1384
|
+
`Result.sequence` takes an array of `Result`s and transform it into a `Result` of an array.
|
1385
|
+
If all elements of the array is `Result::Success`, then the method returns `Result::Sucess` containing array of values,
|
1386
|
+
otherwise the method returns `Result::Failure` containing first error.
|
1387
|
+
|
1388
|
+
```ruby
|
1389
|
+
include Mayak::Monads
|
1390
|
+
|
1391
|
+
values = [
|
1392
|
+
Result::Success[String, Integer].new(1),
|
1393
|
+
Result::Success[String, Integer].new(2),
|
1394
|
+
Result::Success[String, Integer].new(3)
|
1395
|
+
]
|
1396
|
+
Result.sequence(values) # Result::Success[String, T::Array[Integer]](@value=[1, 2, 3])
|
1397
|
+
|
1398
|
+
values = [
|
1399
|
+
Result::Success[String, Integer].new(1),
|
1400
|
+
Result::Failure[String, Integer].new("Error1"),
|
1401
|
+
Result::Failure[String, Integer].new("Error2")
|
1402
|
+
]
|
1403
|
+
Result.sequence(values) # Result::Failure[String, Integer](@failure="Error")
|
1404
|
+
```
|
1405
|
+
|
1406
|
+
### Do-notation
|
1407
|
+
|
1408
|
+
`Result` monad does not supports do-notation. The fact that `Result` has two type-parameters makes
|
1409
|
+
it not possible to implement type-safe do-notation in the same fashion as it done for `Maybe` and `Try`.
|