Relaxed assignment conversions for sealed types

Alan Malloy amalloy at google.com
Fri Oct 23 18:35:57 UTC 2020


I missed your earlier message, and I have one question that probably I
would already know the answer to if I were keeping up with the sealed type
specs. Is the permits clause of a sealed type public information, or an
implementation detail? That is, imagine

public sealed interface Foo permits A {}

public class A  implements Foo {}

How does a client from outside of the package perceive this? Of course they
know Foo is sealed and they cannot implement it. I assume they also know
about A, since if they wanted to they could look at A and see it implements
Foo. But do they know whether there is some secret B implementation also?
Do they know whether there are 0, 1, or more non-public implementations of
Foo? It seems like clients would need to know that in order for this idea
to work. If you want it only to work in the same compilation unit as Foo,
that seems simpler but may annoy clients, who can "see" that there is only
one implementation, so why shouldn't they be able to auto-cast it.

On Fri, Oct 23, 2020 at 11:25 AM Brian Goetz <brian.goetz at oracle.com> wrote:

> No one bit on this, but let me just point out a connection that may help
> motivate this: sealed types are union types.
>
> If we say
>
>     sealed interface A permits X, Y { }
>
> then this is like:
>
>     A = X | Y
>
> A structural interpretation of union types says that
>
>     X <: I  &  Y <: I   -->    A <: I
>
> Essentially, this idea says we can borrow from the union nature of sealed
> types when convenient, to provide better type checking.
>
> As mentioned below, the real value of this is not avoiding the cast, but
> letting the type system do more of the work, so that if the implicit
> assumption is later invalidated, the compiler can catch save us from
> ourselves.  A cast would push assumption failures to runtime, where they
> are harder to detect and potentially more costly.
>
>
> On 10/9/2020 11:16 AM, Brian Goetz wrote:
>
> Here's an idea that I've been thinking about for a few days, it's not
> urgent to decide on now, but I think it is worth considering in the
> background.
>
> When we did expression switch, we had an interesting discussion about what
> is the point of not writing a default clause on an optimistically total
> enum switch (and the same reasoning applies on switches on sealed types.)
> Suppose I have:
>
>     var x = switch (trafficLight) {
>         case RED -> ...
>         case YELLOW -> ...
>         case GREEN -> ...
>     }
>
> People like this because they don't have to write a silly default clause
> that just throws an silly exception with a silly message (and as a bonus,
> is hard to cover with tests.)  But Kevin pointed out that this is really
> the lesser benefit of the compiler reasoning about exhaustiveness; the
> greater benefit is that it allows you to more precisely capture assumptions
> in your program about totality, which the compiler can validate for you.
> If later, someone adds BLUE to traffic lights, the above switch fails to
> recompile, and we are constructively informed about an assumption being
> violated, whereas if we had a default clause, the fact that our assumption
> went stale gets swept under the rug.
>
> I was writing some code with sealed classes the other day, and I
> discovered an analogue of this which we may want to consider.  I had:
>
>     public sealed interface Foo
>         permits MyFooImpl { }
>     private class MyFooImpl implements Foo { }
>
> which I think we can agree will be a common enough pattern.  And I found
> myself wanting to write:
>
>     void m(Foo f) {
>         MyFooImpl mfi = (MyFooImpl) f;
>         ...
>     }
>
> This line of code is based on the assumption that Foo is sealed to permit
> only MyFooImpl, which is a valid assumption right now, since all this code
> exists only on my workstation.  But some day, someone else may extend Foo
> to permit two private implementations, but may not be aware of the time
> bombs I've buried here.
>
> Suppose, though, that U were assignable to T if U is a sealed type and all
> permitted subtypes of U are assignable to T.  Then I'd be able to write:
>
>     MyFooImpl mfi = f;
>
> Not only do I not have to write the cast (the minor benefit), but rather
> than burying the assumption "all implementations of Foo are castable to
> MyFooImpl" in implementation code that can only fail at runtime, I can
> capture it in a way the compiler can verify on every recompilation, and
> when the underlying assumption is invalidated, so is the code that makes
> the assumption.  This seems less brittle (the major benefit.)
>
> This generalizes, of course.  Suppose we have:
>
>     sealed interface X permits A, B { }
>     class A extends Base implements X { }
>     class B extends Base implements X { }
>
> Then X becomes assignable to Base.
>
> I'm not quite sure yet how to feel about this, but I really do like the
> idea of being able to put the assumptions like "X must be a Y" -- which
> people _will_ make -- in a place where the compiler can typecheck it.
>
>
>
>
>
>
>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.java.net/pipermail/amber-spec-experts/attachments/20201023/0a906dd0/attachment.htm>


More information about the amber-spec-experts mailing list