Conditional members

Brian Goetz brian.goetz at oracle.com
Fri Apr 8 23:20:30 UTC 2016



On 4/8/2016 4:55 PM, Bjorn B Vardal wrote:
>
> Applicability check
>
> > When loading a parameterization of a generic class, we perform an 
> applicability check for each member as we
>
> > encounter it; in the model outlined here, this is a straight 
> subtyping check of the current parameterization against
>
> > the restriction domain.
>
> In order to support the subtyping check, the applicability check 
> should happen in the specializer, and not when loading the 
> specialization. Both the type information and the class hierarchy are 
> more easily accessible at that point.
>

Agree that this is a specialization-time decision.  But I'm not sure 
what "the specializer" is; in our prototype, we have a class that takes 
a byte[] and a set of bindings and produces a new byte[] which we call 
the specializer, but that's just one (bad) implementation strategy.  So 
I'm not sure that "a specializer" is a necessary part of the story, but 
yes, this is a decision made at specialization time.

> > If there are duplicate applicable members in a classfile where 
> neither's restriction domain is more specific than the
>
> > other's, then the VM is permitted to make an arbitrary choice.
>
> Seconding Karen's comment, we'd like to avoid "arbitrary" choices, as 
> both users and JVM implementers need to know how to get consistent 
> behaviour. Unspecified behaviour may change, and it may also have 
> corner cases that are treated differently by different JVM 
> implementations.
>
> Would it be better to reject a specialization where there are multiple 
> maximally specific applicable members, or to reject templates that 
> would allow such scenarios?
>

We plan to reject these at compile time in any case.  The only question 
is, if some other compiler produces a classfile where there are multiple 
applicable specializations, do we want to reject it on principle?  It's 
more work to reject the classfile than to make an arbitrary choice.  
Consider this classfile:

Where[T=String] void m() {}
Where[U=String] void m() {}
Where[T=String, U=String] void m() {}

This classfile is valid.  But we don't know that until we've read all 
the way to the bottom; when we hit the second m(), we would have to 
record "crap, if I don't see an m() that is better than both the first 
one and second one by the end, I'll have to bail."  That means 
accumulating state as we read the classfile that then has to be 
validated at the end.  Whereas, if we do the purely local thing, we 
avoid this check.  It's your call, but I didn't want to specify 
something that had a cost to prevent something mostly harmless that 
happens rarely.


> Reflection
>
> We need to specify what the reflection behaviour will be for 
> conditional members, as it may depend on how each JVM implementation 
> decides to represent species internally. The current reflection 
> behaviour is not well specified, and adding conditional members may 
> add more inconsistencies.
>

Yep....
>
> JVMTI / class redefinition / class retransformation
>
> This applies conditional members specifically, and also to 
> specializations in general.
>
> What happens when a generic class is redefined? Will the whole 
> specialization nest require redefinition, or will the redefinition be 
> limited to redefined specialization? What about changes to a generic 
> class (template)? What happens if the restriction domain of a 
> conditional members changes?
>

We need to be careful with the terminology, which is hard because we 
don't have good terminology yet.

We have "source classes" that are compiled into "class files", each of 
which may define more than one "runtime type", which can be reflected 
over with "runtime mirrors".  All of these are called "classes" :(

Since there's no artifact for a specialization, I don't think we will 
support redefinition for a specialization; we'd support redefinition for 
a class FILE.  And I think the logical consequence is that we then have 
to redefine all extant specializations of that classFILE, since the 
change could potentially affect all specializations.

> *Any-interface*
> Will only non-conditional methods be in the any-interface? Or will 
> conditional methods have a default implementation (e.g. throw 
> UnsupportedOperationException)?
Yes; the any-interface represents only total members.
>
> Motivation
>
> I think the API migration concern is compelling. But to handle that, 
> it's sufficient to be able to restrict members to the all-erased 
> specialization (or else require them to be total). This mechanism 
> could be very simple, and the resulting API differences seem to be 
> well justified by the compatibility requirements.
>

If that were only the case .... :(

Yes, the most egregious examples will be when a method is simply 
unsuitable for non-reference parameters, and indeed a simpler "where 
all-erased" criteria would fit the bill here, and it's a pretty 
compelling place to want to stop.

Here's another migration concern.  We want to migrate Streams such that 
IntStream and friends can be deprecated.  Pipelines like:

     List<String> strings = ...
     strings.stream().map(String::length)

can now result in a Stream<int> rather than a Stream<Integer> (yay!), 
but in order to retire IntStream, there are some methods, like 
sum/min/max, that are pretty hard to let go of.

Ideally I'd rather slice along a more abstract dimension (e.g., "where T 
extends Arithmable"), but that's a whole new bag of problems that I'd 
like to not couple to this one.

Let's say that we agree that the conditionality selectors should be "as 
simple as possible" and we're going to work within the target use cases 
to determine exactly what that means....

The current proposal is pretty simple, in that it is a pure subtyping 
test, is amenable to a meet-rule, and doesn't include not-combinators.  
But I agree its not the only place we could land, and we're open to 
exploring this further.

> In general I like the idea of a facility that allows for method 
> implementations to be specialized for known types. It can help to get 
> performance in cases where otherwise some abstraction would get in the 
> way by forcing us to treat things uniformly. And the spirit of such 
> specialization is that it should be (at least mostly) transparent, so 
> users shouldn't usually need to think about how the implementation is 
> selected in this case.
>
> However, at the Java language level, conditional members have a 
> significant limitation here. Erasure means that it's only possible to 
> specialize for primitive types. There's no way to specialize for 
> String, for example.
>
> Then there is type-specific functionality such as List<int>.sum(). 
> This doesn't strike me as something that belongs in List, any more 
> than these do:
>
> - List<String>.append()
>
> - List<List<T>>.append()
>
> - List<UnaryOpeartor<T>>.compose()
>
> But due to erasure, these wouldn't be expressible. This kind of API 
> extension is limited to primitive types. (Later it could be done for 
> value types more generally, but I don't think it would be good to 
> allow users to special-case their own APIs for user-defined value 
> types, but not for T=String.)
>

Actually, this isn't true (but your general argument about "Is this the 
language feature we're looking for" is still entirely valid.) We can 
express this *up to erasure*, just as we can with everything else.

If we have, at the source level, a member conditioned on an erased 
parameterization:

     <where T=String>
     void append(T t) { ... }

this is fine.  We erase String to 'erased' in the classfile (so it 
becomes "where erased T"), but the compiler can enforce that it is only 
invoked when T=String, just as we do with:

     <T extends Bar> T m(T t) { .... }

In the classfile, the arg and return types are Object, but the compiler 
will reject the call if T != String.  Where this runs out of gas is 
overloads that are erasure-equivalent:

     <where T=String>
     void append(T t) { ... }

     <where T=Integer>
     void append(T t) { ... }

which the compiler will reject with the familiar, if frustrating, "can't 
overload these methods, they have the same erasure" error. So I think we 
can (if we want) extend this treatment to reference types, up to where 
erasure gets in the way.


> We would get the fluent style of call "(...).sum()", but I don't think 
> adding methods to List is the right way to get that, especially if it 
> will only work for primitive types, and if it means that users need to 
> think about sometimes methods of List more often than necessary.
>

I would, in fact, be disinclined to add such methods to List.  But 
Stream<T> -- which is about *computation*, not *data*, seems a different 
case.  In fact, in addition to the terrible performance that boxed 
streams have, telling Java developers that they should sum a stream with

     ...reduce(0, (x,y) -> x+y)

was likely, we felt, to engender this response (warning, NSFW):

     http://s.mlkshk-cdn.com/r/FTAF

Still open to better ideas, though.







More information about the valhalla-spec-observers mailing list