P A
12/01/2022, 3:47 AMsomething
and somethingElse
use context receivers(context(EffectScope<A>)
and `context(EffectScope<C>)`respectively) as there is no obvious way to mapLeft A and C within the either
block to, say, map A and C to a new error hierarchy Z(EffectScope<Z>
).simon.vergauwen
12/01/2022, 7:18 AMcontext(Raise<A>)
fun ServiceA.something(): B = ...
context(Raise<C>)
fun ServiceB.somethingElse(b: B): D = ...
context(Raise<A>, Raise<C>)
fun result(): D {
val b = serviceA.something()
return serviceB.somethingElse(b)
}
You can then handle errors independently, or all at the same time. I.e.
// 2.x.x syntax, backporting to 1.x.x
context(Raise<C>)
fun resolveA(): D {
recover({ result() }) { a ->
println(a)
// raise(a.toC())
fallbackD()
}
}
// 1.x.x syntax
context(Raise<C>)
fun resolveA(): D = effect<A, D> {
result()
}.getOrElse {
println(a)
fallbackD()
}
simon.vergauwen
12/01/2022, 7:19 AMRaise
with EffectScope
for the current name. The 1.x.x backport I am working on now will also be named Raise
like 2.x.xP A
12/01/2022, 7:47 AMsimon.vergauwen
12/01/2022, 7:59 AMfun resolveBothCAndA(): D = effect<C, D> {
effect<A, D> {
result()
}.getOrElse { a ->
println(a)
fallbackD()
}
}.getOrElse { c ->
println(c)
fallbackD()
}
All this syntax works, and looks the same, for Either
. You can even call either.bind()
inside of effect
as well, since the either { }
DSL works trough the effect
DSL.
The only downside of Either
is that you cannot have two errors in the Left
type argument. If Kotlin got unions types you could do Either<A | C, D>
but we don't have that atm.
What you can also do is create tree like hierarchies, so errors have a common parent. You can find an example here, https://github.com/nomisRev/ktor-arrow-example/blob/main/src/main/kotlin/io/github/nomisrev/DomainError.ktP A
12/03/2022, 7:22 AMP A
12/03/2022, 7:26 AMP A
12/03/2022, 7:31 AMP A
12/03/2022, 7:34 AMP A
12/03/2022, 7:38 AMsimon.vergauwen
12/03/2022, 8:08 AMThe downside of Either in this context is also the downside of Effect correct?Yes, and no.
Effect
only represents suspend context(Raise<E>) () -> A
. In 2.x.x it's even a simple typealias
and as discussed above context receiver
doesn't have this limitation. So using context receivers with Raise
are the best abstraction, and arguable the simplest encoding since it will never require functional operators like bind
.
context(Raise<E1>, Raise<E2>)
fun program(): A
Sadly context receivers might still take some time, a year or more before becoming fully stable 😞 This of course also doesn't take away that union types could be handy, since it could eliminate the need for fold
and instead translate program
here to E1 | E2 | A
allowing you to then work with when
which I believe would be really idiomatic.simon.vergauwen
12/03/2022, 8:11 AMEffect
as typealias Effect<E, A> = suspend Raise<E>.() -> A
in the 2.x.x branch (and backport to 1.x.x).
That is actually a performance optimisation as well, since the compiler can optimise suspend lambdas
and it avoids the allocation of the interface Effect
.simon.vergauwen
12/03/2022, 8:14 AMAllow me to share a complete working example of what I mean by being forced to handle more error cases than necessary.That is indeed quite a lot of boilerplate, in this case I would rely on Kotest matchers and simply use
obj.*shouldBeTypeOf*
which avoids all this boilerplate.