Anyone else had issues already with forgetting .bi...
# arrow
a
Anyone else had issues already with forgetting .bind() calls when using Either for error handling with Arrow? Are there any best practices or tips to avoid this? (especially for unit results which means the results are not needed further and thus the compiler cannot help)
s
There is a really cool Detekt integration for this, https://github.com/woltapp/arrow-detekt-rules
We should really put this on the website 🤔 I'm praying for context receivers to come sooner than later thought 😄
a
good to know. this one of the most frequent errors we have when it comes to why an error handler did not trigger properly when it comes to the more rare outages / error situations
context receivers will be able to catch this at compile time?
s
No, but context receivers would allow you to avoid
Either
and thus also
bind
. In combination with Arrow and the awesome DSL support we have in Kotlin it'll offer a modern and functional alternative to typed exceptions. Something that in (modern) FP is sometimes referred to as Effects (Handlers).
a
that indeed sounds like the missing piece when it comes to error handling so that's something that is in development right now by the arrow team?
s
An example, this can already be done today as well but with more limitations (1 context).
Copy code
object MyError

fun Raise<MyError>.example(): Int =
  raise(MyError)

fun Raise<MyError>.two(): Int =
  one() + one()

// Materialise
either {
  two()
} shouldBe Either.Left(MyError)
Yes, it's actually already complete and is something that you've already been using under the hood in 1.0.0. We've heavily improved it towards 2.0.0, and explored it with all our experience and feedback we got throughout 1.0.0. TL;DR:
EffectScope
&
EagerEffectScope
have been flattened into
Raise
and there is no longer a need to distinct between the two. Neither between
either { }
and
either.eager { }
. We've added a bunch of DSLs around
Raise
which was possible due to the much more flexible implementation, so it's easy to install error handlers etc. I.e. (continues on above example)
Copy code
fun recovered(): Int =
  recover({ one() }) { _: MyError -> 0 }

object OtherError

fun Raise<OtherError>.mapsError(): Int =
  recover({ one() }) { raise(OtherError) }

fun Raise<MyError>.wrapDatabase(): User =
  catch({ query("...").toUser() } { t: PSQLException -> raise(MyError) }
All previous code like
either { }
, with
bind()
, etc will all remain valid and all these DSLs are useable and compatible with each other. So no previous code needs to be changed, and you can adopt whatever you prefer or makes sense for your use-cases / team preference.
Raise
is what powers the
either { }
DSLs.
a
ah. thank you very much for this detailed write-up. but
either { }
is still deprecated and will be removed for
effect { }
, right?
s
No,
either { }
is not deprecated (and it never was?). With these new changes
Effect
becomes a
typealias Effect<E, A> = suspend Raise<E>.() -> A
. So the equivalent of the function signatures I wrote above, but as a lambda. The problem with
suspend
lambdas is that they don't have great support except for
suspend () -> A
, and with
effect { }
we can also leverage
@BuilderInference
so
effect { }
is just a utility function for providing better syntax when constructing these lambdas. Besides it becoming a simple typealias, leveraging lambdas instead of it being an
interface
also offers a bunch of performance benefits. This makes
either { }
&
bind
cost-free, and "as-expensive" as a single
flatMap
. However
Either
is still a
val
where
Effect
is a lambda. So you could actually say that
Either
is the natural result of execution the
Effect
lambda. Making it concrete can be done by
effect { }.toEither()
which is the (~) implementation of
either { }
.
a
ah, the deprecation was just a matter of changed package
and what will be needed to "enable" the Raise DSL .. or does it automagically enable itself due to the Raise type?
s
Nothing is needed ☺️ All code is source-compatible, so simply changing package/imports will be sufficient. We're still ironing out some small kinks before releasing the next minor version, last before 2.0.0 to provide a smooth (and lengthy) transition period. Here is a Migration Script, with a repo testing it. I'm also investigating if we can instead leverage OpenRewrite for an even better experience, but its Kotlin support seems still a bit experimental. You can try it already in an alpha release if you want,
1.1.6-alpha.39
. Alpha releases are guaranteed to be binary compatible with 1.x.x series, but unreleased code is prone to breaking changes.
a
interesting. yeah will definitely give it a shot. thank you very much! 🙂
s
That works too, but it still fails if you do this without calling bind. Of course, if you depend on
x
it then fails to compile.
Copy code
either {
  val x = returnsEither()
  1
}
A second problematic case could still be a program returning
Unit
, since
() -> Unit
can swallows any returned value 😕
Copy code
either<String, Unit> {
  returnsEither()
}
It's still an improvement without requiring Detekt, but requires putting
CheckResult
annotations on code. Not sure if Kotlin has module-wide annotations like Java for
NotNull
? 🤔 Never tried that.
e
good catch 🙂 Another proposal in the thread is to have the inverse, explicit "DiscardableReturn" or similar..
Anyway, detekt rules are a great start... I think it would've saved us from several hours of debugging at at least two occassions so far
s
In the future I hope to have IDEA support through a FIR compiler plugin, but I've been saying that while waiting for K2 for 3+ years now 😅 That way we can provide inspections like this, with
alt+enter
refactoring.

https://i.kym-cdn.com/photos/images/newsfeed/001/373/328/b16.jpg