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