mayak 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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`.