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

Introduce SubclassOptInRequired to the codebase #4115

Merged
merged 12 commits into from
May 28, 2024
Merged

Conversation

dkhalanskyjb
Copy link
Contributor

The initial stage of #3770

Marked:

  • Job,
  • Deferred and CompletableDeferred,
  • Flow, SharedFlow, StateFlow,
  • CloseableCoroutineDispatcher,
  • CancellableContinuation,
  • All of their public implementors.

@qwwdfsad
Copy link
Contributor

qwwdfsad commented Apr 26, 2024

Postponed until Kotlin 2.0 because this feature doesn't work as intended now

@dkhalanskyjb
Copy link
Contributor Author

No idea why the testCoroutinesTimeoutInheritanceWithNoTimeoutInDerived test could fail as a result of these changes.

@dkhalanskyjb
Copy link
Contributor Author

dkhalanskyjb commented May 16, 2024

Possibly a fluke. After a restart, the tests passed.

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.

It feels like we have a mix of concepts here.

We have SubclassOptInRequired that is "Hey, we are not sure if we won't add new methods/change the contract. Be wary!" (examples: SharedFlow, maybe Deferred), and there are "These were never meant to be extended, but they technically might be because of the language restrictions or our omission" (Job, CancellableContinuation, ChannelFlow).

It feels off to have both covered with Brittle* with WARNING level.

For the later, I suggest using SubclassOptInRequired(InternalCoroutinesApi::class), for the former -- keep it as is, but maybe rename it to something more straightforward -- ExperimentalForInheritanceCoroutinesApi (or UnstableFor*).

@@ -13,6 +13,8 @@ import kotlin.coroutines.*
* This class is generally used as a bridge between coroutine-based API and
* asynchronous API that requires an instance of the [Executor].
*/
@OptIn(ExperimentalSubclassOptIn::class)
@SubclassOptInRequired(BrittleForInheritanceCoroutinesApi::class)
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a stable class with no mentions of unstable inheritance in the doc. Should we really mark it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

With CloseableCoroutineDispatcher being its typealias, I see no way around it.

@@ -173,6 +173,8 @@ import kotlin.coroutines.*
* These implementations ensure that the context preservation property is not violated, and prevent most
* of the developer mistakes related to concurrency, inconsistent flow dispatchers, and cancellation.
*/
@OptIn(ExperimentalSubclassOptIn::class)
@SubclassOptInRequired(BrittleForInheritanceCoroutinesApi::class)
Copy link
Contributor

Choose a reason for hiding this comment

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

Frankly, taking into account that Flow is the way to program and that it has been here for a long time, I doubt we ever can change that, and this warning might be just a headache to opt-in. I suggest we might give up on that

Copy link
Contributor Author

Choose a reason for hiding this comment

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

People who want custom Flow implementation should use AbstractFlow instead, so I think it's okay to discourage inheriting from Flow.

Copy link
Contributor

Choose a reason for hiding this comment

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

I still argue that Flow is neither brittle nor experimental.

In my mental model, it's either Delicate (because of the invariants that must be preserved) or nothing (because it's quite hard to rationalize it otherwise). WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In my mental model, it's certainly brittle, because it's a mistake to try to implement it without using AbstractFlow. It's not Delicate because we don't give realistic means of fulfilling the contracts in custom Flow instances that don't inherit from AbstractFlow.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

by-delegation to Flow does happen occasionally (https://grep.app/search?q=%20%3A%20Flow%3C%5B%5E%3E%5D%2B%3E%20by&regexp=true), and the following code does emit a warning:

public class MyFlow: Flow<Int> by flowOf(1, 2, 3) {
}

So, Flow probably shouldn't be marked as InternalForInheritance, at least, as we may want to make it difficult to opt in to that eventually, but this benign pattern should work without issues.

@dkhalanskyjb
Copy link
Contributor Author

Let's look at it in a case-by-case basis:

  • Job is not an internal API, so I think it would be surprising to have This is an internal kotlinx.coroutines API when you try to inherit from it. We could extend the description of InternalCoroutinesApi with If you get this issue when trying to inherit, this means that inheriting is specifically banned or something like that, but I think this would overcomplicate the meaning of the annotation.
  • Deferred and CompletableDeferred inherit from Job, so must be guarded by at least that strict a warning, which means they are also in the "must never be inherited from" category.
  • Flow, SharedFlow, StateFlow are probably also interfaces that must never be inherited from directly: I don't see what one could gain from implementing Flow and not inheriting from AbstractFlow. If we think SharedFlow and StateFlow are useful things to extend, we probably must also provide the corresponding abstract classes.
  • CloseableCoroutineDispatcher is the one clear-cut thing in this list: it's reasonable to extend it, but not stable.
  • CancellableContinuation is in the same basket as Job.

To follow the spirit of your suggestion, I introduced another annotation that strongly states that this shouldn't be inherited from and applied it to Job, CancellableContinuation, and Deferred.

@RequiresOptIn(
level = RequiresOptIn.Level.WARNING, message =
"This is a kotlinx.coroutines API that is not intended to be inherited from, " +
"as the library may handle predefined instances of this in a special manner."
Copy link
Contributor

Choose a reason for hiding this comment

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

I would suggest keeping it consistent: InternalForInheritance (or InternalForImplementation) and adding the corresponding disclaimer to the message: No compatibility guarantees are provided. It is recommended to report your use-case of internal API to kotlinx.coroutines issue tracker, so stable API could be provided instead

"Either new methods may be added in the future, which would break the inheritance, " +
"or correctly inheriting from it requires fulfilling contracts that may change in the future."
)
public annotation class BrittleForInheritanceCoroutinesApi
Copy link
Contributor

Choose a reason for hiding this comment

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

We now have three terms in our glossary -- experimental, brittle and unstable.

I suggest sticking with experimental as the most straightforward one -- most of our "Not stable for inheritance" mean "as new methods might be added to this interface in the future"

@@ -173,6 +173,8 @@ import kotlin.coroutines.*
* These implementations ensure that the context preservation property is not violated, and prevent most
* of the developer mistakes related to concurrency, inconsistent flow dispatchers, and cancellation.
*/
@OptIn(ExperimentalSubclassOptIn::class)
@SubclassOptInRequired(BrittleForInheritanceCoroutinesApi::class)
Copy link
Contributor

Choose a reason for hiding this comment

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

I still argue that Flow is neither brittle nor experimental.

In my mental model, it's either Delicate (because of the invariants that must be preserved) or nothing (because it's quite hard to rationalize it otherwise). WDYT?

Copy link
Contributor

@whyoleg whyoleg left a comment

Choose a reason for hiding this comment

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

mostly just questions, feel free to ignore, if the questions are too silly :)

The initial stage of #3770

Marked:
* Job,
* Deferred and CompletableDeferred,
* Flow, SharedFlow, StateFlow,
* CloseableCoroutineDispatcher,
* CancellableContinuation,
* All of their `public` implementors.
Before this change, there was just one annotation: APIs that can't
be inherited from. Now, it's more nuanced: some APIs are never
planned to be available for inheritance and are marked as such.
@dkhalanskyjb dkhalanskyjb force-pushed the dk-subclassoptin branch 3 times, most recently from bdbd0c7 to 9e65346 Compare May 28, 2024 09:18
*/
@OptIn(ExperimentalSubclassOptIn::class)
@SubclassOptInRequired(markerClass = InternalForInheritanceCoroutinesApi::class)
Copy link
Contributor

Choose a reason for hiding this comment

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

This is going to be big, I guess.
https://grep.app/search?q=Deferred%3C%5B%5Cw%5D%2B%3E%20by&regexp=true it's used in the wild and previously we got reports about : Deferred<> by .. not working because of our internal type assertions.

@dkhalanskyjb dkhalanskyjb merged commit 61dd23c into develop May 28, 2024
1 check passed
@dkhalanskyjb dkhalanskyjb deleted the dk-subclassoptin branch May 28, 2024 11:00
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

3 participants