A sealed type and its permitted subtypes may have different access modifiers, by design.  For example, we expect cases like the following, where the implementations are encapsulated, to be common:

    public sealed interface Foo permits FooImpl { }
    private class FooImpl implements Foo { }

The permits list of a sealed type X is visible to anyone to whom X is visible; it's just that they may not be able to access those classes.  (You can say Class.forName("X") and get an answer even when X is not accessible, you just won't be able to operate on it.  Similarly, you can refer to an inacessible class in source code, and you'll get an error like "class not accessible", rather than "class not found.")

If we have:

    public sealed interface Foo permits A, B { }
    public class A implements Foo { }
    private class B implements Foo { }

and an arms-length client did a switch over Foo:

    var x = switch (foo) {
        case A: ...
    }

the compiler would know it is not exhaustive, because it knows there are additional subtypes.  Within the package of Foo, it could switch over them all:

    var x = switch (foo) {
        case A: ...
        case B: ...
    }

and the compiler will see it is exhaustive.

So, sealing is largely independent of accessibility.  If everyone is inside the circle, you get the type extra checking that sealing provides; to someone outside the circle, they can see what they can see, and know there is stuff they don't see.

But, these "relaxed conversions" apply between two types, and you have to be able to access both types in order to write the assignment anyway.  So I think it is not an issue?


On 10/23/2020 2:35 PM, Alan Malloy wrote:
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.go...@oracle.com <mailto:brian.go...@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.








Reply via email to