Norbi
02/25/2023, 9:16 AMraulraja
02/25/2023, 9:37 AMcontext(Raise<String>)
fun foo(): Int = raise("error")
vs
fun foo(): Result<Int, String> = Result.Failure("error")
raise is like throwing exception but you throw typed values which get caught by the receiver scope providing and alternative return to the function.Norbi
02/25/2023, 9:56 AMRuntimeException
...
EDIT: As I see raise() is still in development unfortunately, I'm looking for the best solution for Kotlin 1.8.0 that is already available :|raulraja
02/25/2023, 10:36 AMRaise
is a rename and rework of the internal for what is now available as EffectScope
in the current Arrow stable release.
https://arrow-kt.io/docs/apidocs/arrow-core/arrow.core.continuations/-effect/fun EffectScope<String>.foo(): Int = shift("error")
Norbi
02/25/2023, 10:37 AMraulraja
02/25/2023, 10:39 AMRaise
got recently backported so it may already be available as RaiseNorbi
02/25/2023, 10:45 AMI will try it for sure, although it seems to be a huge restriction that the extension receiver is "used up" by this construct. I hope that multi-platform context receivers will land in Kotlin 1.9 at last...fun EffectScope<String>.foo(): Int = shift("error")
simon.vergauwen
02/25/2023, 12:12 PMRaise
is available in 1.1.6-alpha.28
and will be released in 1.2.0
in 3-ish weeks.
It is indeed a bit restrictive, but luckily you can still return Either<String, Int>
when you need to free up the receiver, and then mix it back into Raise<String>
using bind
when you need too. These patterns mix seamlessly in Arrow, so you can pick and choose what you need when you need it.
fun EffectScope<String>.userId(): UserId = 1
fun Repo.fetch(id: UserId): Either<String, User> =
shift("user with $id not found")
fun EffectScope<String>.program(): Unit {
val id = userId()
val user = Repo.fetch(id).bind()
println(user)
}
Ties
02/27/2023, 7:59 AMNorbi
02/28/2023, 1:32 PMfrom the point of view of ergonomics is worse than checked exceptions because you no longer use regular syntax to deal with the result, instead you have to deal with a wrapped value.Wouldn't it be better to support some kind of checked failures (similar to checked exceptions) with a better syntax instead of the fairly sophisticated implementation of
raise()
(that is based on RaiseCancellationException
under the hood anyway)?
What do you think about the following if the language would support it with a hypothetical throws
declaration and throw()
function:
sealed interface FetchError
object UserNotFound: FetchError
data class DatabaseError(val cause: RuntimeException): FetchError
throws(FetchError)
fun Repo.fetch(id: UserId): User =
try {
findByIdOrNull(id)
} catch (e: RuntimeException) {
throw(DatabaseError(e))
}
?: throw(UserNotFound)
fun program() {
val user = try {
repo.fetch(id)
} catch (e: UserNotFound) { // It would be required to handle all errors
...
} catch (e: FetchError) { // This would catch the rest, in this example only DatabaseError
...
}
println(user)
}
EDIT: of course this is just a quick idea, eg. it would be better to use other names for throws
, throw()
and catch() {}
like raises
, raise()
and handle() {}
.
Please note that I'm not a functional expert, so I may miss some fundamental problem with my idea :)simon.vergauwen
02/28/2023, 1:52 PMRaise
has a fairly sophisticated implementation 😅 but I may be biased as the author.
Regardless of that, of course would having first-class support in the language be awesome. I would argue that this comes very close though, but if the language would every adopt something like that we'd drop Raise
in a heartbeat in favor of it.
EDIT: of course this is just a quick idea, eg. it would be better to use other names forTranslating your snippets to context receivers.,throws
andthrow()
likecatch() {}
,raises
andraise()
.handle() {}
typealias FetchErrors = Raise<FetchError>
sealed interface FetchError
object UserNotFound: FetchError
data class DatabaseError(val cause: RuntimeException): FetchError
context(FetchErrors)
fun Repo.fetch(id: UserId): User =
try {
findByIdOrNull(id)
} catch (e: RuntimeException) {
raise(DatabaseError(e))
}
?: raise(UserNotFound)
fun program() {
val user = effect {
repo.fetch(id)
}.fold({ e: FetchError ->
when(e) {
is UserNotFound -> ...
is DatabaseError -> ...
}
}, ::println)
}
Is arguably comes extremely close without first-class support. The first function is unchanged, and only the place where it provides the handlers is a bit different.Norbi
02/28/2023, 2:14 PMthrows(...)
should be context(...)
, if maybe somebody finds this great conversation in the future 😄)simon.vergauwen
02/28/2023, 2:16 PMNorbi
02/28/2023, 4:04 PMsimon.vergauwen
02/28/2023, 4:28 PMtry { } finally { }
due to how Coroutines are implemented in the language.
CancellationException
is another restriction since otherwise it doesn't behave correctly in the face of Kotlin Coroutines, which is difference from 0.13.0
to 1.0.0
.
Using Coroutines/`@RestrictSuspension` offered a nice (mostly) theoretical benefit, and the implementation I shared uses simply exceptions which is the difference from 1.0.0
to 2.0.0
(or 1.2.0
new package.)
I'm planning to put this in a more lengthy document, and with code examples etc in an appendix on the website later this year.Norbi
02/28/2023, 4:35 PMarrow.core.continuations.DefaultEffect
where start/suspendCoroutineUninterceptedOrReturn()
is used.)simon.vergauwen
02/28/2023, 4:35 PM