Proposed API for JEP 259: Stack-Walking API

Timo Kinnunen timo.kinnunen at gmail.com
Fri Nov 6 04:38:36 UTC 2015


I had thought the wildcard would get captured and then trivially found to match StackFrame exactly, but I guess that’s available only for the first level wildcards. Which means I’ve been wrongly thinking wildcard typing to be way less constrained than it apparently is. Wow, they had one job, to be usable wherever the actual types don’t matter, and they kinda suck at it.

Maybe wildcards are not the most permissive types after all, but if not, then what is? I was curious so I decided to find out. These are the StackWalker implementations that compile and can call a Function which will then call Stream::count. All of them have identical body blocks:

		<T> T jog(Function<? super Stream<? extends StackFrame>, T> function) {}
		<T> T walk(Function<? super Stream<StackFrame>, T> function) {}
		<T> T strut(Function<? super Stream<? super StackFrame>, T> function) {}
		<T> T lollop(Function<Stream<? extends StackFrame>, T> function) {}
		<T> T powerWalk(Function<Stream<StackFrame>, T> function) {}
		<T> T somnambulate(Function<Stream<? super StackFrame>, T> function) { return function.apply(Stream.of("one", "two").map(StackFrame::new)); }

To win, a Stream::count holding Function has to compile with as many implementations as possible. And it turns out we have one winner, with a perfect score! The winner is… deftCounter(): 

	static <S extends Stream<?>> Function<S, Long> deftCounter() { return Stream::count; }

We also have a loser, scoring no points at all! And the loser is… deftCounter’s ugly cousin, meekCounter:

	static Function<? extends Stream<?>, Long> meekCounter() { return Stream::count; }

I’m not going to draw too many conclusions based on this one instance, but it’s an interesting outcome nonetheless. 






(also, somnambulate means sleepwalking)


Sent from Mail for Windows 10



From: Maurizio Cimadamore
Sent: Thursday, November 5, 2015 23:34
To: Timo Kinnunen;Remi Forax;Paul Sandoz
Cc: core-libs-dev at openjdk.java.net
Subject: Re: Proposed API for JEP 259: Stack-Walking API




On 04/11/15 11:19, Timo Kinnunen wrote:
> Hi,
>
> I tested your version of the wildcard counter and it appears to be incompatible with one possible signature of the StackWalker.walk method.
>
> Here’s my code. Please see the line with ERROR comment:
>
> 	static class StackWalker {
> 		static class StackFrame implements CharSequence {
> 			// ...
> 		}
> 		<T> T walk(Function<? super Stream<StackFrame>, ? extends T> function, IntUnaryOperator sizing) {
> 			return function.apply(Stream.of("one " + sizing.applyAsInt(1), "two" + sizing.applyAsInt(2)).map(StackFrame::new));
> 		}
> 		<T> T powerWalk(Function<Stream<StackFrame>, T> function, IntUnaryOperator sizing) {
> 			return function.apply(Stream.of("one " + sizing.applyAsInt(1), "two" + sizing.applyAsInt(2)).map(StackFrame::new));
> 		}
> 	}
>
> 	static Function<Stream<?>, Long> wildCounter() {
> 		return Stream::count;
> 	}
> 	static <T> Function<Stream<T>, Long> tameCounter() {
> 		return Stream::count;
> 	}
> 	public static void main(String[] args) {
> 		Function<Stream<StackWalker.StackFrame>, Long> tameCounter = tameCounter();
> 		Function<Stream<?>, Long> wildCounter = wildCounter();
> 		Long counted1 = new StackWalker().walk(tameCounter, i -> i);
> 		Long counted2 = new StackWalker().walk(wildCounter, i -> i);
> 		Long counted3 = new StackWalker().powerWalk(tameCounter, i -> i);
> 		Long counted4 = new StackWalker().powerWalk(wildCounter, i -> i); // ERROR
> 		System.out.println(counted1 + counted2 + counted3 + counted4);
> 	}
>
> This may be a problem with javac, but it doesn’t exactly inspire confidence in me towards wildcards. More like it’s another example in a long-running pattern of bad behavior. The version by Paul Sandoz compiles without issues, no surprises there as usual.
>
> And this is what javac is telling me:
>
> Information:Using javac 1.8.0_65 to compile java sources
> Error:(77, 50) java: method powerWalk in class nonnulls.GenericTest.StackWalker cannot be applied to given types;
>    required: java.util.function.Function<java.util.stream.Stream<nonnulls.GenericTest.StackWalker.StackFrame>,T>,java.util.function.IntUnaryOperator
>    found: java.util.function.Function<java.util.stream.Stream<?>,java.lang.Long>,(i)->i
>    reason: cannot infer type-variable(s) T
>      (argument mismatch; java.util.function.Function<java.util.stream.Stream<?>,java.lang.Long> cannot be converted to java.util.function.Function<java.util.stream.Stream<nonnulls.GenericTest.StackWalker.StackFrame>,T>)
Right - you are passing a Function<Stream<?>, Long> where a 
Function<Stream<StackFrame>, T> is expected - regardless of what happens 
to the inference variable T, for this to be valid, it must be that 
Stream<?> == Stream<StackFrame>, which is obviously not the case. 
There's no doubt that '? super' is needed in the argument position for 
Function - as to the '? extends' in the return type I think it's 
irrelevant - i.e. this:

<T> T powerWalk(Function<? super Stream<StackFrame>, T> function, IntUnaryOperator sizing) {


and

<T> T powerWalk(Function<? super Stream<StackFrame>, ? extends T> function, IntUnaryOperator sizing) {


Should result in having same applicability.

Maurizio
>
>
>
> Sent from Mail for Windows 10
>
>
>
> From: Remi Forax
> Sent: Wednesday, November 4, 2015 10:04
> To: Paul Sandoz
> Cc: core-libs-dev at openjdk.java.net
> Subject: Re: Proposed API for JEP 259: Stack-Walking API
>
>
> Hi Paul,
>
> The use of BaseStream was just an example, here is another one that works only if the function first parameter type is declared as '? super Stream<StackWalker.StackFrame>'.
>
> static Function<Stream<?>, Integer> counter() {
>    return stream::count;
> }
>
> ...
> StackWalker walker = ...
> int count = walker.walk(counter());
>
> regards,
> Rémi
>
> ----- Mail original -----
>> De: "Paul Sandoz" <paul.sandoz at oracle.com>
>> Cc: core-libs-dev at openjdk.java.net
>> Envoyé: Lundi 2 Novembre 2015 13:44:24
>> Objet: Re: Proposed API for JEP 259: Stack-Walking API
>>
>> I agree with Maurizio, the first signature is good enough.
>>
>> One could argue that it might be better to apply PECS since it would
>> encourage more consistent usage when it is actually required as all too
>> often it’s easy to forget, and then too late to change. However, i don’t
>> want to encourage the use of BaseStream since it was an unfortunate mistake
>> that this made public.
>>
>> Paul.
>>
>>> On 2 Nov 2015, at 13:26, Maurizio Cimadamore
>>> <maurizio.cimadamore at oracle.com> wrote:
>>>
>>> So, we have three potential signatures here:
>>>
>>> <T> T walk(Function<Stream<StackWalker.StackFrame>, T> function) //1
>>>
>>> <T> T walk(Function<Stream<StackWalker.StackFrame>, ? extends T> function)
>>> //2
>>>
>>> <R extends T, T> T walk(Function<Stream<StackWalker.StackFrame>, R>
>>> function) //3
>>>
>>>
>>> Under normal conditions (i.e. lambda parameter being passed to 'walk') I
>>> think all these signatures are fundamentally equivalent; (2) and (3) seem
>>> to have been designed for something like this:
>>>
>>> Number n = walk(s -> new Integer(1));
>>>
>>> That is, the function returns something that is more specific w.r.t. what
>>> is expected in the return type of walk. But - in reality, if 'walk'
>>> returns an R that is a subtype of T (as in the third signature), then walk
>>> also returns a T (as R is a subtype of T), so the result value can be
>>> passed where a T is expected.
>>>
>>> The converse example:
>>>
>>> Integer n1 = walk(s -> (Number)null);
>>>
>>> Similarly fails on all three signatures.
>>>
>>>
>>> More generally, with all such signatures, T will always receive:
>>>
>>> * lower bound(s) (from the return value(s) of the lambda passed as a
>>> parameter to the 'walk' method)
>>> * one upper bound (from the target-type associated with the 'walk' call.
>>>
>>> Under such conditions, the upper bound will always be disregarded in favor
>>> of the lower bounds - meaning that instantiation of T will always be
>>> driven by what's inside the lambda. Signature (3) mentions different
>>> variables (R and T) but the end result is the same - as the bound says R
>>> extends T - meaning that lower bounds of R are propagated to T - leading
>>> to exactly the same situation.
>>>
>>>
>>> In other words, I don't think there are obvious examples in which the
>>> behavior of these three signatures will be significantly different - if
>>> the return type of 'walk' would have been a generic type such as List<T>,
>>> the situation would have been completely different - with with a 'naked'
>>> T, I think the first signature is good enough, and the third is, in my
>>> opinion, making things harder than what they need to be.
>>>
>>> I think the second signature is not necessary, from a pure type-system
>>> perspective; but I guess one could make an argument for it, but purely in
>>> terms of consistency with other areas (after all, the second
>>> type-parameter of a Function is in a covariant position).
>>>
>>> I hope this helps.
>>>
>>> Maurizio
>>>
>






More information about the core-libs-dev mailing list