Skip to content

Instantly share code, notes, and snippets.

@maxcruz
Created May 19, 2020 06:41
Show Gist options
  • Save maxcruz/68fd297cb352e00544337ed3cf7de02b to your computer and use it in GitHub Desktop.
Save maxcruz/68fd297cb352e00544337ed3cf7de02b to your computer and use it in GitHub Desktop.
Either suspend composition
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