One of my project is throwing-lambdas; in it I aim to ease the use of potential @FunctionalInterface
s in Stream
s, whose only "defect" for being used in streams is that they throw checked exceptions (on my part I'd rather call defective the fact that you can't throw checked exceptions from streams but that's another story).
So, for Function<T, R>
I define this:
@FunctionalInterface
public interface ThrowingFunction<T, R>
extends Function<T, R>
{
R doApply(T t)
throws Throwable;
default R apply(T t)
{
try {
return doApply(t);
} catch (Error | RuntimeException e) {
throw e;
} catch (Throwable throwable) {
throw new ThrownByLambdaException(throwable);
}
}
}
This allows me to define, for instance:
final ThrowingFunction<Path, Path> = Path::toRealPath;
(why Path::toRealPath
... Well, precisely because it has an ellipsis).
Not wanting to stop here I want to be able to write stuff like:
Throwing.function(Path::toRealPath).fallbackTo(Path::toAbsolutePath)
The above NEARLY works... Read on.
I also define this:
public abstract class Chainer<N, T extends N, C extends Chainer<N, T, C>>
{
protected final T throwing;
protected Chainer(final T throwing)
{
this.throwing = throwing;
}
public abstract C orTryWith(T other);
public abstract <E extends RuntimeException> T orThrow(
final Class<E> exclass);
public abstract N fallbackTo(N fallback);
public final <E extends RuntimeException> T as(final Class<E> exclass)
{
return orThrow(exclass);
}
}
And this is the implementation of it for Function
s:
public final class ThrowingFunctionChain<T, R>
extends Chainer<Function<T, R>, ThrowingFunction<T, R>, ThrowingFunctionChain<T, R>>
implements ThrowingFunction<T, R>
{
public ThrowingFunctionChain(final ThrowingFunction<T, R> function)
{
super(function);
}
@Override
public R doApply(final T t)
throws Throwable
{
return throwing.doApply(t);
}
@Override
public ThrowingFunctionChain<T, R> orTryWith(
final ThrowingFunction<T, R> other)
{
final ThrowingFunction<T, R> function = t -> {
try {
return throwing.doApply(t);
} catch (Error | RuntimeException e) {
throw e;
} catch (Throwable ignored) {
return other.doApply(t);
}
};
return new ThrowingFunctionChain<>(function);
}
@Override
public <E extends RuntimeException> ThrowingFunction<T, R> orThrow(
final Class<E> exclass)
{
return t -> {
try {
return throwing.doApply(t);
} catch (Error | RuntimeException e) {
throw e;
} catch (Throwable throwable) {
throw ThrowablesFactory.INSTANCE.get(exclass, throwable);
}
};
}
@Override
public Function<T, R> fallbackTo(final Function<T, R> fallback)
{
return t -> {
try {
return doApply(t);
} catch (Error | RuntimeException e) {
throw e;
} catch (Throwable ignored) {
return fallback.apply(t);
}
};
}
}
So far so good (although IDEA fails to recognize the code of orTryWith()
as valid, but that's another story).
I also define a utility class called Throwing
and the problem lies in the main()
of this class which I wrote as a test:
public final class Throwing
{
private Throwing()
{
throw new Error("nice try!");
}
public static <T, R> ThrowingFunctionChain<T, R> function(
final ThrowingFunction<T, R> function)
{
return new ThrowingFunctionChain<>(function);
}
public static void main(final String... args)
{
// FAILS TO COMPILE
final Function<Path, Path> f = function(Path::toRealPath)
.fallbackTo(Path::toAbsolutePath);
}
}
Now, the error message for the code above is:
Error:(29, 48) java: incompatible types: cannot infer type-variable(s) T,R
(argument mismatch; invalid method reference
method toRealPath in interface java.nio.file.Path cannot be applied to given types
required: java.nio.file.LinkOption[]
found: java.lang.Object
reason: varargs mismatch; java.lang.Object cannot be converted to java.nio.file.LinkOption)
Error:(29, 49) java: invalid method reference
non-static method toRealPath(java.nio.file.LinkOption...) cannot be referenced from a static context
Error:(30, 25) java: invalid method reference
non-static method toAbsolutePath() cannot be referenced from a static context
I can't diagnose the exact cause of the error here, but to me it just looks like ellipsis gets in the way; in fact, if I do:
final ThrowingFunctionChain<Path, Path> f = function(Path::toRealPath);
try (
final Stream<Path> stream = Files.list(Paths.get(""));
) {
stream.map(f.fallbackTo(Path::toAbsolutePath))
.forEach(System.out::println);
}
then it compiles: so it means that Stream.map()
does acknowledge the result as being a Function
...
So why won't Throwing.function(Path::toRealPath).fallbackTo(Path::toAbsolutePath)
compile?
As Holger has stated in the comments, the compiler is limited in its type inference when chaining methods. Just provide an explicit type argument
One of the problems is that, being a varargs method,
Path::toRealPath
has the following different types (acting as implicit overloads) for target type inference purposes:Path toRealPath(Path a)
(anew LinkOption[0]
would be implicitly supplied as second parameter by the compiler).Path toRealPath(Path a,LinkOption... b)
(the second parameter is direct).Path toRealPath(Path a,LinkOption[] b)
(the second parameter is direct).Path toRealPath(Path a,LinkOption b)
(the second parameter would be supplied by the compiler asnew LinkOption[1] { b }
).Path toRealPath(Path a,LinkOption b,LinkOption c)
(the second parameter would be supplied by the compiler asnew LinkOption[2] { b, c }
).Path toRealPath(Path a,LinkOption b,LinkOption c,LinkOption d)
(the second parameter would be supplied by the compiler asnew LinkOption[3] { b, c, d }
)Path toRealPath(Path a,LinkOption b,LinkOption c,LinkOption d,LinkOption e)
(the second parameter would be supplied by the compiler asnew LinkOption[3] { b, c, d, e }
)etc
(the second parameter would be supplied by the compiler asnew LinkOption[n] { b, c, d, e, ... }
)The other is that solving the type equation implied by statement
Function<Path,Path> f= function(Path::toRealPath).fallbackTo(Path::toAbsolutePath) ;
would require inferring the type parameters forfallbackTo
so its return type conformsFunction<Path,Path>
, and then the type parameters forfunction
so its own return type conforms to the first. Java makes this kind of inference, but only when a single step is involved (argument to parameter, return value to return type, right to left side in assignments). In the case of return types, the inference chain is unbounded and usually has more than one solution.The alternative is to provide a little more type information to the compiler. For example:
Or by creating temporary variables as in:
In this case the declaration of
stage1
provides the additional information.In a much more general vein, I don't completely understand why one could want to propagate exceptions when expecting them as the norm. I have done more or less the same with
Optional<T>
or by using a very small extension to it that is able to encapsulate exception information. You can even use the "suppressed exception" mechanism introduced in 1.7 to handle try-with-resources exceptions happening during implicit calls toclose
methods. The problem seems to be exactly the same. The code is very simple and fully compatible with thestreams
and other standard Java SE frameworks.Your code fragment
is hitting a limitation of Java 8’s type inference which is contained in the specification, so it’s not a compiler bug. Target typing does not work when you chain method invocations. Since the first method of the chain is a varargs method, its target type is required to find the intended invocation signature. This situation similar to when you write
p->p.toRealPath()
, where the number of parameters of the invocation is unambiguous but the type ofp
is not known. Both won’t work in an invocation chain (besides in the last invocation)This can be resolved by either making the type of the first invocation explicit,
or
or
or by converting the method invocation chain into unchained method invocations as described here:
The specification deliberately denied the type inference for two invocations when one invocation targets the result of the other but it works if the same expressions are just parameters of another invocation.