Relaxed assignment conversions for sealed types
Remi Forax
forax at univ-mlv.fr
Sun Oct 25 12:55:38 UTC 2020
I've not answered to Brian series of emails because i'm not able to figure out if it's a good thing or not.
One thing to know that Brian did not say is that we may need to see a sealed super type as a subtype for Valhalla.
Currently generics doesn't support primitive object type (ex inline type), like it doesn't support 'int'.
So the compiler generates a sealed super type which is a reference type that can be used instead (not unlike Integer is used instead of int).
If we have a rule that allows semless conversions between the super reference type and primitive object type,
the pair super reference type/primitive object type will behave more or less like the auto-boxing rules between an Integer and an int.
Now, Tagir, you have talk about about having a special syntax or using a switch at use site,
there is also another solution which is to declare a special relationship at declaration site.
sealed interface MyFoo permits MyFooImpl { }
autobox class MyFooImpl implements MyFoo { }
with autobox (or whatever keyword you want) that means
- verifies that i'm the only implementation
- allow unboxing conversion from MyFoo to MyFooImpl
You can see autobox as a kind of handsake between the sealed type and its sole implementation that this is the only implementation.
One issue I see with this proposal, and the original one by Brian, the relation is asymetric,
MyFooImpl to MyFoo is subtyping but MyFoo to MyFooImpl is unboxing (so not the same pass when finding the most specific method), so it doesn't work fully like Integer and int.
And i've used unboxing because it's already an existing rule, but maybe it's a new kind of conversion that will require us to carefully think about
regards,
Rémi
----- Mail original -----
> De: "Tagir Valeev" <amaembo at gmail.com>
> À: "Brian Goetz" <brian.goetz at oracle.com>
> Cc: "amber-spec-experts" <amber-spec-experts at openjdk.java.net>
> Envoyé: Dimanche 25 Octobre 2020 02:18:06
> Objet: Re: Relaxed assignment conversions for sealed types
> Hello!
>
> I'm a little bit scared of introducing this kind of implicit cast into
> the assignment operation. This looks like the right operand in
> MyFooImpl mfi = f; becomes a poly-expression, as the type of the f
> expression now depends on the mfi type. How will it interact with
> other constructs? E.g. can we write MyFooImpl getFooImpl(MyFoo f)
> {return f;}? Or aMethodAcceptingFooImpl(f)? What about something that
> requires more inference, like Function<MyFoo, MyFooImpl> = f -> f; ?
>
> I believe this would be either a very special rule applicable to very
> specific constructors (e.g. variable declaration only) or it will
> overcomplicate the type inference, possibly introducing new constructs
> where all the types cannot be unambiguously inferred. In the former
> case, this could be confusing. People would ask: why it works in
> declarations but doesn't work in assignments/method
> calls/returns/lambdas?
>
> If we want a compile-time check that 'this cast is safe as there's
> exactly one implementor', I think this should be done in a more
> explicit way. Btw we already designed such a way:
> MyFooImpl mfi = switch(f) {case MyFooImpl _mfi -> _mfi;};
> This would do the thing, though may look somewhat ugly. We can add
> some sugar if anybody doesn't like this syntax. Probably like
> MyFooImpl mfi = (safe-cast-to MyFooImpl)f;
> But my point is that this kind of downcast should be explicit.
>
> I'm also not sure that this construct will be necessary so often.
> First, it's for internal use only, so the surface is quite limited:
> the clients don't need it. Second, even in internal uses, it's likely
> that in most of the cases the public API of interface Foo will be
> enough, so the downcast won't be necessary. Third, it would not be so
> hard to create a private one-liner method for this:
>
> private class MyFooImpl {
> private static MyFooImpl asImpl(Foo f) {
> return switch (f) {case MyFooImpl mfi -> mfi;};
> }
> }
>
> This will keep a compile-time check and allow downcasting in any
> context. E.g. even in qualifiers, like
> asImpl(f).methodThatExistsInImplOnly().
>
> I don't think we should overcomplicate the language here.
>
> With best regards,
> Tagir Valeev.
>
> On Fri, Oct 9, 2020 at 10:16 PM Brian Goetz <brian.goetz at oracle.com> 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.
>>
>>
>>
>>
>>
More information about the amber-spec-experts
mailing list