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.
@@ -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`.