-
Notifications
You must be signed in to change notification settings - Fork 64
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Discuss: Use coroutines instead of exceptions for binding #93
Comments
Thanks for opening this discussion. This is very interesting and to be honest I agree with a lot of your points, e.g. avoiding the use of exceptions. With regards to the On the topic of the exception stacktrace, the Looking at the implementations in your PR is also confusing me, the distinction between the coroutines vs kotlinx.coroutines is not clear to me. The main difference seems to be that one uses Looking at the two beneath they both seem to have the same constraints on their public sealed interface ResultSuspendableBindingScope<E> {
public suspend fun <V> Result<V, E>.bind(): V
}
@RestrictsSuspension
public sealed interface ResultBindingScope<E> {
public suspend fun <V> Result<V, E>.bind(): V
} Another small thing is that I'm pretty sure the code beneath would be incorrect - we would have to catch the potential exception and wrap it in an override fun resumeWith(result: kotlin.Result<V>) {
finalResult = Ok(result.getOrThrow())
} Overall the 'pure coroutines' implementation looks cleanest to me, but I don't know what the tradeoff is. I'd be very keen to understand which one you think is better and why, which one improves the developer experience for consumers, and what the tradeoffs between all three of the solutions (existing and the two proposed ones) are. |
Apologies for the confusion, this is a limitation of using coroutines. The current exception-based solution handles this case, but if using coroutines it would not allow you to bind unless you are within a suspend function.
Here I was not referring to CancellationExceptions. I was referring to the fact that if you throw an exception within a coroutine and view the stacktrace, you get extra lines which indicate where the continuation code happened. Not a big deal, but could be slightly more confusion for people trying to analyze the stack trace.
The distinction is similar to that of a sequence builder and a flow builder. For example the following is does not compile: suspend fun test() = withContext(Dispatchers.IO) {
println("Hello")
}
sequence {
test()
yield("Hey")
} However, in a flow it will compile: suspend fun test() = withContext(Dispatchers.IO) {
println("Hello")
}
flow {
test()
emit("Hey")
} This is because flows can handle both coroutine contexts for kotlinx.coroutines and coroutine context from the flow builder. The same is true between the pure coroutines binding and the kotlinx coroutines binding.
You're right, the naming here wasn't well thought through as they both "suspend". Perhaps
Why do you want to catch the exception? We are not doing a runCatching here. If someone throws inside a Re: restrictions on where you can call bind, I learned recently that you cannot "bind" in rust when you're in a separate closure. For example `map` in an iterator. That would be closer to how this POC implementation works compared to the current one. I want to make it clear that I'm not confident that this is the right solution for this library. Overall, I think that using pure coroutines feels more correct to me than using an exception, but there's still some research to be done around performance. Most of my points around performance are inferences, but I don't really have any data to prove that this solution is any better from that perspective. Plus the downsides of not being able to call this from a non-suspending function would be pretty disruptive to users of this library. Feel free to close this issue if you want, I think it could be an interesting solution, but I think there's still more research to be done and I don't really have the time to pursue it at the moment. |
Hi!
I would've posted this in discussion if it was available, but I was curious if it had ever been considered to use coroutines instead of exceptions for the binding blocks.
Using exceptions has always seemed slightly error prone to me since all it takes is for someone to catch a generic exception and you now potentially have unexpected behavior. In addition stacktraces can be slightly expensive to produce.
On the flip side, coroutines have their drawbacks as well. For example, the stack traces can sometimes be confusing and there are limitations for where you can actually use bind.
For example I made a proof of concept and tried it out in a repo that made decent usage of binding. The issues are that any function that is not inline or suspend will produce a compilation error because you need to be within the custom coroutine context.
A map function is okay because it's inline, but a sequence function is not
The other issue I discovered is when the binding context is put into the receiver of another function it fails as well (this is only an issue when restricting the scope of what suspend functions can be called from your scope).
doesn't work but
In conclusion, I'm not convinced yet whether one solution is better than the other. I think the coroutine based approach has some benefits over the exception based approach, but it's slightly more restrictive in how you use it. I'm curious to know what you and others think! Thanks for your time.
Here's a link to my POC if you want to try it out. I made both a pure kotlin.coroutines version and a kotlinx.coroutines version.
dronda-t/kotlin-result@master...dronda-t:kotlin-result:dronda/kotlin-coroutines
The text was updated successfully, but these errors were encountered: