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.





Reply via email to