Skip to content
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

Rework CompletionHandler to avoid subclassing a functional type #4010

Merged
merged 6 commits into from
Feb 27, 2024

Conversation

dkhalanskyjb
Copy link
Contributor

Workaround for KT-64075

@dkhalanskyjb
Copy link
Contributor Author

I don't know why Lincheck tests fail with a LinkageError. @ndkoval, could you help?

@dkhalanskyjb
Copy link
Contributor Author

Status: it seems like the fix should be on the side of Lincheck. We probably won't have to change the approach taken in this PR.

@dkhalanskyjb dkhalanskyjb marked this pull request as ready for review February 14, 2024 14:32
Copy link
Contributor

@qwwdfsad qwwdfsad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great job, thank you for taking care of this nuisance.

Looks good, all my remarks are trivialities about the doc

) : CancelHandler() {
override fun invoke(cause: Throwable?) {
handler.invoke(cause)
internal interface CancelHandler: NotCompleted {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice doc 👍

*/
// TODO: deprecate. This doesn't seem better than a simple function type.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we?

I see this type alias (or maybe even I would interpret it as an opaque type if it was possible) as a perfect fit -- it is a functional type, but with very specific semantics that is a property of this "type", not of the callsite (invokeOn* and coroutine builders), and the documentation to this type is a perfect place to spell it out.

WDYT? As usual, this particular endpoint might be not the most important one, but it's nice to align our mental models here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's my reasoning behind the TODO.

tl;dr: this function's contract is defined by the places that accept the function, not the other way around, so these other places should define what they expect. Also, the latest commit that fixed the explanation of what the argument means, and this type looks even more unnatural now.

Long version:

I know of two use cases for binding an arbitrary function to a name:

  • Passing it somewhere so that that other place calls it,
  • Storing it somewhere so that someone calls it later.

For the second use case, introducing a type alias is clearly beneficial: when you have a var handler: CompletionHandler, there is some contract that should connect the parties. If I see a CompletionHandler, I may only invoke it so or so, and return, it promises me this and that, and vice versa. But what we have here is purely the first use case.

but with very specific semantics that is a property of this "type", not of the callsite

This surprises me greatly: I think it's exactly the opposite.

The contract on the handler is exactly the combination of the contracts of the functions that accept the handler. Here's the deciding factor for me: if the behavior of invokeOnCompletion changed and the contract on CompletionHandler could somehow be relaxed, we wouldn't define a RelaxedCompletionHandler (supertype of CompletionHandler), we would just change the documentation of CompletionHandler. So, CompletionHandler is a "fake" type, a product of circumstances, only useful for passing it to the specific call sites.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the elaborate answer.

we would just change the documentation of CompletionHandler.
The contract on the handler is exactly the combination of the contracts of the functions that accept the handler.

Indeed. It appears I just think about this one inside out -- its definition/contract is driven bu the need of its "clients" (invokeOnCompletion), yet the moment the contract is established -- it's the property of CompletionHandler that it does not throw, not the vise versa -- "in general, CompletionHandler is whatever you want it to be, but it shouldn't throw if passed to Job.invokeOn...()"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I got your intention correctly, you're touching on a deep concept here, called the native type theory. In essence, we can take any dynamically typed function and define its type as a function that accepts all values that don't make it misbehave. By this logic, fun LocalDate(year: Int, monthNumber: Int, dayOfMonth: Int) is equivalent to a total function of the type fun LocalDate(year: Year<TypeLevelYearNumber>, monthNumber: MonthNumber<TypeLevelMonthNumber>, dayOfMonth: DayOfMonth<MonthNumber = TypeLevelMonthNumber, YearNumber = TypeLevelYearNumber>).

I think the reasons CompletionHandler is not worth introducing are the same reasons typealias MonthNumber = Int isn't worth introducing. I can elaborate on them if you want.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's quite close. I don't see it fit e.g. for an arbitrary function, but here we have a multitude of functions that all accept the same kind of parameter with the same kind of constraints. These constraints are significant enough to document, and broad enough to get tired of repeating it (NB: here, I focus not on "repeating the text" but on "repeating the conceptual complexity of the entity accepting this parameter and draining our complexity budget").

It's just lightweight and specific enough so it doesn't deserve a dedicated nominal type.

(please note that this discussion is not a blocker of this PR by any means)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here we have a multitude of functions that all accept the same kind of parameter with the same kind of constraints

Let's consider two scenarios separately:

  • There's a CompletionHandler and, separately, a CancellationHandler;
  • The two types of handlers stay merged into one, just because they have the same type.

I'm confident the second option is a no-go because the constraints and the behavior are actually different between invokeOnCompletion and invokeOnCancellation. Knowing just the CancellationHandler type, you can't make a judgement about how it will behave, you need to know which of the two endpoints ends up actually receiving the handler.

The first option also seems strange to me: then, the contract on CompletionHandler would simply completely mirror the contract on invokeOnCompletion, and I don't see the benefits of introducing a separate entry point when we can just say "will be passed to invokeOnCompletion and must satisfy the same requirements" and in fact, must either do so or direct people to the documentation of CompletionHandler explicitly, because no one realistically is going to notice, "Aha, this function accepts not just any lambda but one with a special type, let me read the docs."

* - Can be invoked concurrently with the surrounding code.
* - Can be invoked from any context.
*/
fun invoke(cause: Throwable?)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice to mention the cause semantics and when it's a null?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Now the abstraction of CompletionHandler looks even more strange.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it's a mess. Luckily, it's a documented mess right now and a constant reminder we probably should change it :)

@dkhalanskyjb dkhalanskyjb merged commit 390be6f into develop Feb 27, 2024
1 check passed
@dkhalanskyjb dkhalanskyjb deleted the dk-CompletionHandlers-rework branch February 27, 2024 09:18
knisht pushed a commit to JetBrains/intellij-deps-kotlinx.coroutines that referenced this pull request Apr 15, 2024
…in#4010)

Workaround for KT-64075

Also, thoroughly document the contracts imposed on the callbacks provided to `invokeOnCompletion` and `invokeOnCancellation`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants