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.