For discussion, I may be missing something but the...
# arrow
p
For discussion, I may be missing something but the example above seems to get complicated if
something
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>
).
s
With context receivers you can actually compose them though.
Copy code
context(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.
Copy code
// 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()
}
Replace
Raise
with
EffectScope
for the current name. The 1.x.x backport I am working on now will also be named
Raise
like 2.x.x
p
Thanks for the feedback. Using 1.x.x syntax, let’s supposed we wanted to handle the left sides( A and C ) at once within the same function as to not further propagate the context requirements of A and C. How does that code look like?(I’m still learning the Arrow ways, apologies if the question is trivial)
s
You can just nest them inside of each other, so:
Copy code
fun 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.kt
p
@simon.vergauwen The downside of Either in this context is also the downside of Effect correct?
If I’m understanding correctly, not having union types support is a major pain point as it causes all the sealed hierarchy for the code-base(or layer of the code base) to be handled when only certain parts of the hierarchy need handling.
Allow me to share a complete working example of what I mean by being forced to handle more error cases than necessary.
cc: @Chris Lee 🙂, I appreciate your insight.
s
The 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
.
Copy code
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.
Due to the lack of context receivers we define
Effect
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
.
Allow 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.