How do I deal with Function and ellipsis/vararg

2019-04-09 18:39发布

问题:

One of my project is throwing-lambdas; in it I aim to ease the use of potential @FunctionalInterfaces in Streams, 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 Functions:

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?

回答1:

Your code fragment

Function<Path, Path> f = function(Path::toRealPath).fallbackTo(Path::toAbsolutePath);

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 of p 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,

Function<Path, Path> f = Throwing.<Path,Path>function(Path::toRealPath)
  .fallbackTo(Path::toAbsolutePath);

or

ThrowingFunctionChain<Path, Path> f0 = function(Path::toRealPath);
Function<Path, Path> f = f0.fallbackTo(Path::toAbsolutePath);

or

Function<Path, Path> f = function((Path p)->p.toRealPath())
    .fallbackTo(Path::toAbsolutePath);

or by converting the method invocation chain into unchained method invocations as described here:

public static <T, R> ThrowingFunctionChain<T, R> function(
    final ThrowingFunction<T, R> function)
{
    return new ThrowingFunctionChain<>(function);
}
public static <T, R> Function<T, R> function(
    final ThrowingFunction<T, R> function, Function<T, R> fallBack)
{
    return new ThrowingFunctionChain<>(function).fallbackTo(fallBack);
}
public static void main(final String... args)
{
    Function<Path, Path> f = function(Path::toRealPath, Path::toAbsolutePath);
}

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.



回答2:

As Holger has stated in the comments, the compiler is limited in its type inference when chaining methods. Just provide an explicit type argument

final Function<Path, Path> f = Throwing.<Path, Path>function(Path::toRealPath).fallbackTo(Path::toAbsolutePath);


回答3:

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) (a new 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 as new LinkOption[1] { b }).
  • Path toRealPath(Path a,LinkOption b,LinkOption c) (the second parameter would be supplied by the compiler as new LinkOption[2] { b, c }).
  • Path toRealPath(Path a,LinkOption b,LinkOption c,LinkOption d) (the second parameter would be supplied by the compiler as new 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 as new LinkOption[3] { b, c, d, e })
  • etc (the second parameter would be supplied by the compiler as new 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 for fallbackTo so its return type conforms Function<Path,Path>, and then the type parameters for function 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:

Function<Path,Path> f= Throwing.<Path,Path>function(Path::toRealPath).fallbackTo(Path::toAbsolutePath) ;

Or by creating temporary variables as in:

ThrowingFunctionChain<Path,Path> stage1= function(Path::toRealPath) ;
Function<Path,Path> f= stage1.fallbackTo(Path::toAbsolutePath) ;

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 to close methods. The problem seems to be exactly the same. The code is very simple and fully compatible with the streams and other standard Java SE frameworks.