Proposal: Track subtype exhaustiveness for classes with only private constructors #8942
Replies: 28 comments
-
Seems like this would roll in with the general sealed type hierarchy proposals. |
Beta Was this translation helpful? Give feedback.
-
Same idea, but also recognizing an existing pattern |
Beta Was this translation helpful? Give feedback.
-
int M(C c) => s switch
{
A a => 0,
B b => 1
}; Does this still need a null check? -- Strongly in favor of this general area. Though on the fence if i think this special case is worth having support for, or if we should have the more general way to mark any hierarchy such that we can enumerate all the cases for it to do exhaustiveness checking |
Beta Was this translation helpful? Give feedback.
-
Imagine it's prefixed with |
Beta Was this translation helpful? Give feedback.
-
A different way of looking about it: this is a bug fix. It is simply true that any warning provided in the above switch expression is incorrect. The warning is that the switch expression is not exhaustive, but it is, in fact, exhaustive. |
Beta Was this translation helpful? Give feedback.
-
Not quite - this makes the number of subtypes part of the public contract, and adding new subtypes is now a breaking change where it wasn't before. I still think it's worth it, but it does have costs. |
Beta Was this translation helpful? Give feedback.
-
I don't think this is really true, because it's a broader language design opinion at the level of the system, as opposed to a pure consideration of the warning itself. The warning provided in this case is simply incorrect. The warning says that the switch is incomplete, but it isn't. The fact that a user changing their code can produce different behavior on the consumers part is true, but in the language we've never considered that as a "breaking change." |
Beta Was this translation helpful? Give feedback.
-
I'm not saying that this is a breaking change. What I'm saying is that till now, a library author can freely add subtypes to a class without introducing new warnings to consuming code. Now he can't. In general if a library author cannot change something without introducing new warnings, that means this is a public contract, and in general it's best if public contracts are explicit, so authors don't accidentally tie themselves down. In this case I think authors probably would want to introduce new warnings in such a case, so it's worth it. |
Beta Was this translation helpful? Give feedback.
-
@YairHalberstadt I see, so you're talking about the broader, design-level question. As to the question of whether this would count as an "explicit" opt-in to the behavior, I would guess so. But I don't think this negates from my fundamental point that 1) the current warning is incorrect and 2) it's clearly detectable by the compiler. |
Beta Was this translation helpful? Give feedback.
-
Doesn't the same exhaustiveness check apply to internal classes too (or to public types with internal constructors)? All their inheritors will be known at compile time, unless I'm missing something. |
Beta Was this translation helpful? Give feedback.
-
@Richiban I don't think it's possible because of |
Beta Was this translation helpful? Give feedback.
-
Ahh, good point. It might be possible to detect that and treat any internal type defined in such an assembly as 'open' but it complicates the feature a fair bit. Sticking to |
Beta Was this translation helpful? Give feedback.
-
The compiler already do something like this when it's immediately obvious: interface I {
sealed class A {
void M(A a) {
if (a is I) {} // won't warn if A is not sealed, but that would be still correct wrt effective accessibility
}
}
} Without any kind of modifier at the declaration-site to "verify" a closed hierarchy, I think it could get tricky to "infer" such relationship without a few false negatives, unless this is only about recognizing a specific pattern (like private constructors). |
Beta Was this translation helpful? Give feedback.
-
Actually that should still warn unless A and B both are either sealed or define a private ctor. Question is how far the compiler should go to determine all possible subtypes. |
Beta Was this translation helpful? Give feedback.
-
Wouldn't that still be exhaustive @alrz ? Any further subclasses would have do drive from A or B, and both of those are already checked in the switch... |
Beta Was this translation helpful? Give feedback.
-
Agreed, any subtype of |
Beta Was this translation helpful? Give feedback.
-
You're right, was thinking whether or not those could be inherited at all. That doesn't affect this particular switch, but that is also not exactly a closed hierarchy? |
Beta Was this translation helpful? Give feedback.
-
Guess as long as base types are constant it's already close- Is there any other language that allows non-flat DUs? None comes to mind. |
Beta Was this translation helpful? Give feedback.
-
It's closed on the sense that we can tell if they switch is exhaustive without needing a catch all clause :-) |
Beta Was this translation helpful? Give feedback.
-
Java :) |
Beta Was this translation helpful? Give feedback.
-
I mean, does it support exhaustiveness like what is described here? |
Beta Was this translation helpful? Give feedback.
-
Yes, it was an explicit goal of sealed classes:
And the pattern matching in switch expressions is in its third preview with Java 19: public static String shape(Shape shape) {
// this would be a compiler error if considered non-exhaustive.
return switch(shape) {
case Circle x -> "Circle";
case Triangle x -> "Triangle";
case Quadrilateral x -> "Quadrilateral";
};
}
private sealed interface Shape permits Circle, Triangle, Quadrilateral { }
private static final class Circle implements Shape { }
private static final class Triangle implements Shape { }
private static sealed abstract class Quadrilateral implements Shape permits Square, Rectangle { }
private static final class Square extends Quadrilateral { }
private static final class Rectangle extends Quadrilateral { } Add in record patterns, also in preview, and I believe this is the combination of features intended to solve for DUs in the Java language. |
Beta Was this translation helpful? Give feedback.
-
I actually like the approach Java has taken by making the base type declare the legal subtypes rather than trying to determine it through analysis. It more clearly codifies the intent of the developer and the expected contract of the hierarchy. I would expect that if DUs came to the language with an enum-like syntax that would cover that intent, but maybe it should be considered for closed hierarchies too. |
Beta Was this translation helpful? Give feedback.
-
I think that depends on
From the link, that seem to be optional but given the above requirements I think it's mostly predictable? Not sure if there's any restriction on where those subtypes can actually be declared. |
Beta Was this translation helpful? Give feedback.
-
|
Beta Was this translation helpful? Give feedback.
-
That's true, it's optional in the cases where the subtypes are obvious, such as nested within the parent type. But you still have to explicitly specify that the parent type is a sealed/closed hierarchy and the legal subtypes are encoded in the metadata. |
Beta Was this translation helpful? Give feedback.
-
Looking closer, there's another restriction: No such thing exists in C# so the scope is considerably broader. That should be fine, but I'd personally prefer it to be explicit rather than relying on a precise pattern to encode this semantics as a "side effect" of just having a particular accessibility on the constructor - it's too easy to accidentally break the contract. |
Beta Was this translation helpful? Give feedback.
-
Will this fix also apply to any upcoming |
Beta Was this translation helpful? Give feedback.
-
Private class constructor exhaustiveness
Summary
The switch expression already has a notion of exhaustiveness, and there are a variety of cases where switch arms can be confirmed to be exhaustiveness, as defined by reachability of the decision tree.
Right now the only state for reference types tracked for reachability is nullability, e.g. the following switch will not provide a warning about non-exhaustion:
This proposal is to extend tracking to subtypes of classes with only private constructors. For example,
The above should also produce no warnings, as the type checks in the switch expression exhaust all possible values of the input.
Unfortunately, the improvement as specified does have one backwards compatibility problem: it is an error if a switch arm is subsumed by a previous case. This would mean code which currently has a "catch all" pattern to avoid a warning in the above code would now be an error if the change were applied naively. The proposed fix for this is to not provide any warning or error if a case is unreachable only when arbitrary subclasses are not considered reachable.
This proposal should be an unalloyed benefit to programmers -- the compiler is simply smart enough to not provide incorrect warnings.
Design Meetings
Beta Was this translation helpful? Give feedback.
All reactions