Created
May 19, 2020 06:41
-
-
Save maxcruz/68fd297cb352e00544337ed3cf7de02b to your computer and use it in GitHub Desktop.
Either suspend composition
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.mytaxi.arrowplayground | |
import arrow.core.Either | |
import arrow.core.EitherOf | |
import arrow.core.fix | |
import arrow.fx.IO | |
import arrow.fx.extensions.io.async.effectMap | |
import kotlinx.coroutines.delay | |
import kotlinx.coroutines.runBlocking | |
import kotlin.random.Random | |
/* | |
Alternatives to compose Either suspended funtions | |
Sources: | |
https://stackoverflow.com/questions/61806329/how-to-compose-io-functions-with-other-effects-in-kotlin-arrow-fx/61806933#61806933 | |
https://www.pacoworks.com/2019/12/15/kotlin-coroutines-with-arrow-fx/ | |
https://www.47deg.com/blog/arrow-kotlinx-integration/ | |
#arrow channel in kotlinlang Slack | |
The last version of Arrow-kt doesn't support composition of Either returned by suspend functions. | |
*/ | |
data class Loaded(val opeartion: Int) | |
class Failed(opeartion: Int): Exception("$opeartion") | |
suspend fun simulate(result: Boolean, opeartion: Int): Either<Failed, Loaded> { | |
val action = if (result) Either.Right(Loaded(opeartion)) else Either.Left(Failed(opeartion)) | |
delay(Random.nextLong(0, 500)) | |
println("${action}") | |
println() | |
return action | |
} | |
suspend fun init0(): Either<Failed, Loaded> = simulate(true, 0) | |
suspend fun init1(): Either<Failed, Loaded> = simulate(false, 1) | |
suspend fun init2(): Either<Failed, Loaded> = simulate(true, 2) | |
inline fun <A, B, C> EitherOf<A, B>.flatMapInline(f: (B) -> Either<A, C>): Either<A, C> = | |
fix().let { | |
when (it) { | |
is Either.Right -> f(it.b) | |
is Either.Left -> it | |
} | |
} | |
fun main() = runBlocking { | |
//---------------- Why? ----------------// | |
/* | |
This doesn't works bc Either.flatMap is not inlined. | |
This was removed to avoid side-effects and keep functions using the type pure | |
Exceptions thrown in the middle of a map or a flatMap can produce jumping threads or deadlocks | |
val boot = init0() | |
.flatMap { init1() } | |
.flatMap { init2() } | |
*/ | |
//---------------- 1 ----------------// | |
/* | |
Option 1: flatMapInline | |
Add the inlined version of flatMap that was removed from Either | |
Pros: | |
- The easiest and straightforward solution | |
- Keeps the same syntax structure | |
- Less boilerplate | |
Cons: | |
- This is just a trapdoor by the compiler | |
- We are bringing back a deprecated function | |
- It's the same fold function ignoring the failure | |
- The effects are not deferred | |
val bootDeprecated = init0() | |
.flatMapInline { init1() } | |
.flatMapInline { init2() } | |
*/ | |
//---------------- 2 ----------------// | |
/* | |
Option 2: Introduce IO | |
IO is the intended data type to represent side-effects as operations that can be executed | |
lazily, and are capable of failing. | |
Pros | |
- Effects are deferred and executed when IO is bound (great for pure functions) | |
- Specially prepared to work with coroutines promoting suspended composition (eg IO.effect(Dispatchers.Default) { }) | |
- Integration with other extensions and helpers in other Arrow modules (eg suspendCancellable) | |
Cons | |
- It requires another dependency because fx is a separated module | |
- More concepts are introduced into the codebase with the new module | |
- As in my example the init functions are independant, it's required more boilerplate | |
*/ | |
val boot = IO { init0() } | |
.effectMap { if (it.isRight()) init1() else it } | |
.effectMap { if (it.isRight()) init2() else it } | |
println() | |
println("--------------") | |
//println("Result: ${bootDeprecated}") | |
println("Result: ${boot.suspended()}") | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment