Add exit for use in SuspendApp#3917
Conversation
|
Hey @alexandru, The implementation looks goot but I'm a bit unsure about the implicit API by using public interface SuspendAppScope : CoroutineScope {
public fun exit(code: Int)
}
public fun SuspendApp(
context: CoroutineContext = Dispatchers.Default,
uncaught: (Throwable) -> Unit = Throwable::printStackTrace,
timeout: Duration = Duration.INFINITE,
block: suspend SuspendAppScope.() -> Unit,
): Unit = TODO("Existing implementation with updated receiver")
@Deprecated(
message = "Use SuspendApp with SuspendAppScope instead",
level = DeprecationLevel.HIDDEN
)
public fun SuspendApp(
context: CoroutineContext = Dispatchers.Default,
uncaught: (Throwable) -> Unit = Throwable::printStackTrace,
timeout: Duration = Duration.INFINITE,
block: suspend CoroutineScope.() -> Unit,
): Unit = SuspendApp(context, uncaught, timeout) {
block()
}WDYT? |
|
That doesn't actually work because function parameter types are erased on the JVM. You could add a dummy parameter to the new overload, but actually, none of that is necessary. Changing the receiver type to a more specific type like that should be perfectly binary compatible, I believe |
|
I'll try it out, see what works. |
|
You're right @kyay10 it creates a JVM clash so we would have to introduce a Alternatively and maybe better like @kyay10 suggested we just replace the current receiver from Does a receiver DSL solve your problem as well @alexandru? Or did you purposefully design it through a |
|
Another option is to use a context parameter alongside the CoroutineScope receiver, now that they're stable. That'd be a different function arity, so it wouldn't cause a signature clash, and it's a nice way to compose things instead of having an uber-receiver |
|
I would very much like a DSL, but I worry about binary backwards compatibility. The problems happen not for first party code, but rather when updated as a tranzitive dependency. I'm not convinced that going from Maybe there's something I don't understand here, but given that the current type for that input is this: What happens when Arrow gets updated as a transitive dependency, it now expects a |
|
I won't belabor the JVM type erasure point, since we agree on that. Because of contravariance, |
|
@alexandru this is the change in JVM bytecode public final class arrow/continuations/SuspendAppKt {
public static final fun SuspendApp-exY8QGI (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function1;JLkotlin/jvm/functions/Function2;)V
public static synthetic fun SuspendApp-exY8QGI$default (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function1;JLkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
}
+public abstract interface class arrow/continuations/SuspendAppScope : kotlinx/coroutines/CoroutineScope {
+ public abstract fun exit (I)V
+}This is the change in klib: // Klib ABI Dump
// Targets: [iosArm64, iosSimulatorArm64, iosX64, js, linuxArm64, linuxX64, macosArm64, macosX64, mingwX64, tvosArm64, tvosSimulatorArm64, tvosX64, wasmJs, watchosArm32, watchosArm64, watchosSimulatorArm64, watchosX64]
// Rendering settings:
// - Signature version: 2
// - Show manifest properties: true
// - Show declarations: true
// Library unique name: <io.arrow-kt:suspendapp>
+abstract interface arrow.continuations/SuspendAppScope : kotlinx.coroutines/CoroutineScope { // arrow.continuations/SuspendAppScope|null[0]
+ abstract fun exit(kotlin/Int) // arrow.continuations/SuspendAppScope.exit|exit(kotlin.Int){}[0]
+}
-final fun arrow.continuations/SuspendApp(kotlin.coroutines/CoroutineContext = ..., kotlin/Function1<kotlin/Throwable, kotlin/Unit> = ..., kotlin.time/Duration = ..., kotlin.coroutines/SuspendFunction1<kotlinx.coroutines/CoroutineScope, kotlin/Unit>) // arrow.continuations/SuspendApp|SuspendApp(kotlin.coroutines.CoroutineContext;kotlin.Function1<kotlin.Throwable,kotlin.Unit>;kotlin.time.Duration;kotlin.coroutines.SuspendFunction1<kotlinx.coroutines.CoroutineScope,kotlin.Unit>){}[0]
+final fun arrow.continuations/SuspendApp(kotlin.coroutines/CoroutineContext = ..., kotlin/Function1<kotlin/Throwable, kotlin/Unit> = ..., kotlin.time/Duration = ..., kotlin.coroutines/SuspendFunction1<arrow.continuations/SuspendAppScope, kotlin/Unit>) // arrow.continuations/SuspendApp|SuspendApp(kotlin.coroutines.CoroutineContext;kotlin.Function1<kotlin.Throwable,kotlin.Unit>;kotlin.time.Duration;kotlin.coroutines.SuspendFunction1<arrow.continuations.SuspendAppScope,kotlin.Unit>){}[0]You can test this yourself by running |
|
I've been thinking about this for a while. I wonder if we can automate some experimental binary compatibility tests. We can keep around the test jars from previous releases and make sure it all runs successfully (we might also need to change around our tests to have some flag for experimental APIs so their tests don't run for the binary compatibility checks). Effectively, we would have "proof" that, if an API has tests going back to version X, we're still binary compatible with it Of course, this should be an addition to the BCV, not instead of it. This might be way too ambitious for this change, though. I'm fairly confident that this change will be binary compatible (on JVM, to be clear). An easy way to see it is if we break it down into 2:
Step 1 doesn't change any signatures, and it doesn't change the behaviour of the Contrast this with an absolutely breaking change: changing the receiver to |
|
@kyay10 Having tests for binary compatibility would be great. For Scala, we have the Mima plugin, which takes a different approach than Kotlin's ABI validator, as it downloads a previous version and looks at bytecode signatures, although TBH I like Kotlin's ABI validator more. On the topic of the PR, I think my intuition was wrong, will test it when I get to my laptop; as I'm all for the DSL. |
Co-authored-by: Alejandro Serrano <trupill@gmail.com>
Co-authored-by: Alejandro Serrano <trupill@gmail.com>
|
I made the changes, let me know what you think |
exit for use in SuspendApp
nomisRev
left a comment
There was a problem hiding this comment.
Looks good to me @alexandru. Good KDoc as well ❤️
Thank you so much for your first contribution and support to Arrow-kt 🙌
kyay10
left a comment
There was a problem hiding this comment.
We might wanna consider adding a SuspendAppExit interface so that context users can use context(_: CoroutineScope, _: SuspendAppExit) if they so wish, but that can always come later
| uncaught: (Throwable) -> Unit = Throwable::printStackTrace, | ||
| timeout: Duration = Duration.INFINITE, | ||
| block: suspend CoroutineScope.() -> Unit, | ||
| block: suspend SuspendAppScope.() -> Unit, |
There was a problem hiding this comment.
I think it would be possible to maintain binary compatibility here:
- Give a different
JvmNameto the version usingSuspendAppScope, - Create a new overload using
CoroutineScopewithoutJvmName(so the name is maintained).
There was a problem hiding this comment.
This maintains JVM compatibility. I do agree that it breaks Klib compatibility, but we deemed that to be okay (see main conversation).
Simply, SuspendAppScope implements CoroutineScope, so no CCE will happen. Function types are erased on JVM, so no ABI breaks happen
There was a problem hiding this comment.
Please take my suggestion as a simple change that would bring 100% fidelity in JVM. But I don't want it to devolve into discussion about what exactly counts as binary compatible (e.g., should we take reflection into account?), so I'm also fine leaving it as is.
…ontinuations/SuspendApp.kt Co-authored-by: Alejandro Serrano <trupill@gmail.com>
|
Please don't merge it yet, sorry, there's one last thing to settle here... Function input parameters are contravariant, meaning that, because This is why this code should stay compatible, even if Arrow's val myFun: suspend CoroutineScope.() -> Unit = TODO()
//...
SuspendApp(myFun)Contravariance is somewhat counterintuitive, but we should be good. The JVM would consider this backwards compatible even if type erasure wouldn't be a thing. OTOH, I'm having doubts about The reason is that adding abstract methods to an interface does break binary compatibility in a big way. |
|
As @serras already mentioned maybe we should aim to keep 100% fidelity and JVM & KLIB compatibility by keeping the old function with Good point. In Ktor for example we keep all interfaces small or SAM, and add the functionality as extensions.
Not entirely sure what you mean by this? 🤔 I am open to having |
Related to: #3846
This may be premature, and please feel free to ignore my PR, especially as it proposes the introduction of a new public function.
I want a way to exit a
SuspendAppwith a certain exit code (non-zero exit code), and without logging an exception (stderr output should be in my control); so withouthalt, such that graceful shutdown should still work.