<div dir="ltr"><div style="font-family:monospace" class="gmail_default">Hello,<br><br>> I'm not sure I understand the problem you present, are<br>> you saying that if I have the following (I'm on my phone,<br>> so I apologize about the formatting):<br>> <br>> sealed interface S permits A {}<br>> <br>> switch(x) {<br>>     case A a -> ...<br>>     case S s -> ...<br>> }<br>> <br>> Then when I modify S to be:<br>> <br>> sealed interface S permits A, B {}<br>> <br>> My switch expression now has a hidden case? I.e. the<br>> compiler doesn't help me know that I may want to add an<br>> explicit B case?<br><br>Well, not necessarily. By definition, case S s and the default clause hide new inclusions into the type domain. That is their job and we are not trying to take away this functionality.<br><br>The problem is that there are a handful of situations where case S s and default are 1-to-1 matches in matched values. This is a very tricky problem in my eyes because it took me a long time of looking at Robert's example before I realized that the default clause and the case S s are subsets of each other, which is to say they cover the exact same cases - no more and no less.<br><br>> I can see few problems with disallowing a "catchall" clause.<br>> <br>> Here is a textbook example of sealed types:<br>> <br>>     sealed interface Json permits JPrimitive, JArray, JObject {}<br>>     <br>>     sealed interface JPrimitive<T> extends Json permits JNull, JBool, JNumber,<br>>     JString {<br>>         T unbox();<br>>     }<br>>     <br>>     // the implementation of unbox just returns the underlying object, for<br>>     JNull it returns null.<br>>     record JNull() implements JPrimitive<Void> {}<br>>     record JBool(bool val) implements JPrimitive<Boolean> {}<br>>     ...<br>> <br>> Will:<br>> <br>>     switch(x) {<br>>         JArray a -> ...<br>>         JObject o -> ...<br>>         JPrimitive<?> p -> ...<br>>     }<br>> <br>> Be allowed? It has the same problem, if we extend the permits list of<br>> JPrimitive, the compiler won't notify you, but we really want a single<br>> "primitive" clause, semantically we know that it won't ever change (but the<br>> compiler has no way of knowing it).<br><br>So to be clear, the part that Robert is pointing out is that this only applies in a situation where the switch selector expression returns the type S, which is the sealed type. So in the example that you provided, the type of the selector expression would have to be JPrimitive for the situation that Robert is describing to apply (though you address this later).<br><br>> If you only care about the "first layer", there are still<br>> cases were you have a sealed types that permits quite a<br>> lot of types (say, 10, which is a lot but possible), you<br>> may have a function that receive that type, and handle 7<br>> out of the 10 ways the same way, but 3 of the types<br>> require aspecial handling (think subtypes that requires<br>> external resources, or thread locks), not having a<br>> catchall will create a lot of boilerplate (or will make<br>> you switch to if-else chain (pun intended))<br><br>So I think I understand what you mean by first layer, but I will dive into an example anyways to be explicit.<br><br>Let's rework your first example to be exactly what we need -- a sealed type with 10 permitted subclasses. Also, we will apply the restriction that Robert mentioned -- the "case S s" must also be the type of the switch selector expression.<br><br>    sealed interface S permits A, B, C, D, E, F, G, H, I, J { /** Methods, etc. */ }<br>    <br>    record A(long a)   implements S { /** Methods, etc. */ }<br>    record B(int b)    implements S { /** Methods, etc. */ }<br>    record C(short c)  implements S { /** Methods, etc. */ }<br>    record D(String d) implements S { /** Methods, etc. */ }<br>    /** Record E and the rest. */<br>    <br>    final S sealedType = new D("idc");<br><br>Ok, now we have the foundation we need to make examples.<br><br>And let me repeat this part of your quote specifically.<br><br>> you may have a function that receive that type (S), and<br>> handle 7 out of the 10 ways the same way, but 3 of the<br>> types require a special handling (think subtypes that<br>> requires external resources, or thread locks), not having<br>> a catchall will create a lot of boilerplate (or will make<br>> you switch to if-else chain (pun intended))<br><br>Well, let's start off by making sure we accurately capture what you are saying<br><br>    final SomeType output = <br>        switch (sealedType) //from earlier<br>        {<br>        <br>            case A a -> { /** Some complex logic */ }<br>            case B b -> { /** Some complex logic */ }<br>            case C c -> { /** Some complex logic */ }<br>            case S s -> { /** Do something with s that handles the remaining 7 cases all the same way */ }<br>        <br>        };<br><br>Am I capturing your point correctly here? Because if so, why not just do this instead?<br><br>    final SomeType output = <br>        switch (sealedType) //from earlier<br>        {<br>        <br>            case A a -> { /** Some complex logic */ }<br>            case B b -> { /** Some complex logic */ }<br>            case C c -> { /** Some complex logic */ }<br>            default  -> { /** Do something with sealedType that handles the remaining 7 cases all the same way */ }<br>        <br>        };<br><br>Remembed, both (s) and (sealedType) have the type S. So really, anything that we can use (s) for, we can 100% of the time replace it with (sealedType) assuming no when clauses or other fluff.<br><br>That's the point that Robert is getting at here. There is no situation that could be represented with just case S s by itself that could not also be captured with default when the switch selector expression is of type S. Both of these situations are subsets of each other, they cover 100% of the exact same cases. And therefore, he is proposing that we do something when this occurs. He suggests error, I suggest warning. And the error/warning should tell you to use a default clause instead.<br><br>> If you still think it is a problem, I'm sure you could<br>> make a rule in your linter/build pipeline to disallow<br>> catchall clauses<br><br>Can you think of any examples where you would want to have a case S s (excluding when clauses of course) that could not be better communicated and handled with a default clause and using the existing sealedType variable? If there aren't any other cases, then both Robert and I think that this ambiguity should be prevented. The purpose of the default clause is to make it LOUD and clear to the reader that all cases left are to be captured under this clause. To instead use case S s can provide just enough indirection such that if there is enough noise above the switch expression, then this is a pretty easy pothole to miss.<br><br>And based on that, we think that this should be some sort of warning/error on the language level. Not just for some third party linter.<br><br>Thank you for your time and insight!<br>David Alayachew<br></div><br></div>