Does "adapt the formal parameter types of N to the type parameters of M" mean something specific for type systems?

Viewed 97

I'm trying to 100% grok what the Java Language Specification (JLS) means when it says: "...adapting the formal parameter types of N to the type parameters of M..."

Going by what that same section used to say in earlier versions of the JLS, my best guess is that "adapting" in that context means something along the lines of "do something to make the formal parameters of a method compatible with the generic type parameters declared for that method".

I've given the JLS a pretty good search. But it doesn't explain anywhere what that phrase means exactly.

So I gather that the JLS assumes that people immersed in the domain of type systems — language designers, compiler implementers, etc. — would already know the more specific, more technical meaning of "to adapt a formal parameter type".

Please can anybody here break down precisely what "...adapting the formal parameter types of N to the type parameters of M..." means in layman's terms?

TIA.

2 Answers

"Adapting" here is referring to the conversion that takes place because of erasure. Erasure is when the function you have discovered, literally 2 sections below the section you are stumped on, is applied.

<A, B, C extends Number> void dedupersConfusion() {}
<D, E, F extends Number> void dedupersConfusion() {}

The functions is saying that D = A, E = B, and F = C when it's applied. But when is a function like this applied? If you recall in the java tutorial for beginners on the erasure of generic types, which you should probably check out it is applied during erasure.

You are also conflating formal type parameter with type parameters.

<U> void dedupersConflation() {}

U is a formal type parameter of the generic method.

void dedupersConflation(U var) {}

U is a type parameter of the METHOD (notice it's not a generic method). The two example methods you provided in your answer have the same signature regardless of the formal type parameters, because they have the same name and no arguments; you would need to provide your methods with arguments of type parameter for that function to carry any weight in your examples. For example:

<U> void dedupersConfusion(U x) {}
<U extends String> void dedupersConfusion(U x) {}
<U extends Number> void dedupersConfusion(U x) {}

all have different signatures, but the methods:

<U extends Number> void dedupersConfusion(U x) {}
<U extends Number> void dedupersConfusion(U x) {}

have the same signature.

There is another section that goes over the function lub() which allows the function you discovered to work; in the first code snippet of this answer, the least upper bound of A and D is Object and the least upper bound of C and F is Number. But I recommend for you to check out the tutorial for beginners on erasure.

It bugged the hell out of me that that one vaguely-worded bullet point in the Generic Methods section of the JLS was so murky to me.

But after rereading and rereading both older and more recent versions of it, I eventually figured out that what that cryptic bullet point is most likely talking about is what's known in lambda calculus as „alpha renaming“; or α-conversion:

„…α-conversion, sometimes known as α-renaming, allows bound variable names to be changed. For example, α-conversion of λx.x might yield λy.y. Terms that differ only by α-conversion are called α-equivalent. Frequently, in uses of lambda calculus, α-equivalent terms are considered to be equivalent…

So taking this:

Where A1, ..., An are the type parameters of M and B1, ..., Bn are the type parameters of N, let θ=[B1:=A1, ..., Bn:=An]. Then, for all i (1 ≤ in), the bound of Ai is the same type as θ applied to the bound of Bi.

And taking this method as M:

<T, U, V extends T> void m(){ }

And this method as N:

<U, T, V extends T> void m(){ }

Then „renaming each occurrence of a Bi in N's type to Ai” means applying that θ function to each type parameter; with each corresponding i position in lock-step order with each other.

The thing that helped me grok the θ function of 8.4.4 the most, is that the ordinal position of the type parameters is super, super significant in the application of that θ function.

What happens when that θ function is applied to the upper bound of B3 is that extends T must be similarly α-renamed to extends U in order for the type parameter of the upper bound to be in sync with what it was renamed to at index 2:

          +------------------------------+
          ¦    1    ¦    2    ¦    3     ¦
+---------+---------+---------+----------+------------+
|    M    |    T    |    U    |    V     | extends T  |
+---------+---------+---------+----------+------------¦
|    N    |    U    |    T    |    V     | extends T  |
+---------+---------+---------+----------+------------¦
|    θ    |    T    |    U    |    V     | extends U  |
+-----------------------------------------------------+

The upshot being: the signatures of the example methods M and N are not the same. Yes they have the same name (m). And yes they have the same number of type parameters. Even their type parameters have been renamed to be the same and are in the same order. But the kicker is their corresponding type parameters at index 3 have different upper bounds.

So that's what the JLS means by „for all i (1 ≤ in), the bound of Ai is the same type as θ applied to the bound of Bi“. Remember you heard it here first ;)

Related