Is there any way to circumvent the typedness of lambda expressions?
Asked Answered
N

3

9

This is a question that I wondered about since the lambdas had been introduced in Java, and inspired by a related question, I thought that I might bring it up here, to see whether there are any ideas.

(Side notes: There is a similar question for C#, but I did not find one for Java. The questions for Java about "storing a lambda in a variable" always referred to cases where the type of the variable was fixed - this is exactly what I'm trying to circumvent)


Lambda expressions receive the type that they need, via target type inference. This is all handled by the compiler. For example, the functions

static void useF(Function<Integer, Boolean> f) { ... }
static void useP(Predicate<Integer> p) { ... }

can both be called with the same lambda expression:

useF(x -> true);
useP(x -> true);

The expression will once manifest itself as a class implementing the Function<Integer,Boolean> interface, and once as a class implementing the Predicate<Integer> interface.

But unfortunately, there is no way of storing the lambda expression with a type that is applicable to both functions, like in

GenericLambdaTypelambda = x -> true;

This "generic lambda type" would have to encode the type of the method that can be implemented by the given lambda expression. So in this case, it would be

(Ljava.lang.Integer)Ljava.lang.Booleanlambda = x -> true;

(based on the standard type signatures, for illustration). (This is not completely unreasonable: The C++ lambda expressions are basically doing exactly that...)


So is there any way to prevent a lambda expression being resolved to one particular type?

Particularly, is there any trick or workaround that allows the useF and useP methods sketched above to be called with the same object, as in

useF(theObject);
useP(theObject);

This is unlikely, so I assume the answer will plainly be: "No", but: Could there be any way to write a generic, magic adaption method like

useF(convertToRequiredTargetType(theObject));
useP(convertToRequiredTargetType(theObject));

?


Note that this question is more out of curiosity. So I'm literally looking for any way to achieve this (except for custom precompilers or bytecode manipulation).

There seem to be no simple workarounds. A naive attempt to defer the type inference, by wrapping the expression into a generic helper method, as in

static <T> T provide()
{
    return x -> true;
}

of course fails, stating that "The target type of this expression must be a functional interface" (the type can simply not be inferred here). But I also considered other options, like MethodHandles, brutal unchecked casts or nasty reflection hacks. Everything seems to be lost immediately after the compilation, where the lambda is hidden in an anonymous object of an anonymous class, whose only method is called via InvokeVirtual...

Napiform answered 31/3, 2016 at 14:51 Comment(5)
A CallSite? Basically, that is what a lambda isVariometer
Are you looking for a way to convert from an applicable functional interface to the equivalent, desired functional interface, e.g. Function<Integer, Boolean> to Predicate<Integer>? Is this so you can have one lambda stored as a variable of one functional interface type but you can use it as another functional interface type?Sideboard
@Variometer I'm not sure - from looking over the docs, I don't see how this could be applied here. @Sideboard Basically yes. I know that one could wrap it into another lambda, like useP(x -> f.apply(x));, but this does not work generically.Napiform
@JornVernee Quoting from the question: "So I'm literally looking for any way to achieve this (except for custom precompilers or bytecode manipulation)."Napiform
@Napiform Sorry, I got lost in thought after about an hour looking at this :/Akimbo
S
3

I don't see any way of allowing a lambda expression that resolves to one particular functional interface type to be interpreted directly as an equivalent functional interface type. There is no superinterface or "generic lambda type" that both functional interfaces extend or could extend, i.e. that enforces that it takes exactly one parameter of exactly one specific type and returns a specific type.

But you can write a utility class with methods to convert from one type of functional interface to another.

This utility class converts predicates to functions that return booleans and vice versa. It includes identity conversions so that you don't have to worry about whether to call the conversion method.

public class Convert
{
    static <T> Predicate<T> toPred(Function<? super T, Boolean> func)
    {
        return func::apply;
    }

    static <T> Predicate<T> toPred(Predicate<? super T> pred)
    {
        return pred::test;
    }

    static <T> Function<T, Boolean> toFunc(Predicate<? super T> pred)
    {
        return pred::test;
    }

    static <T> Function<T, Boolean> toFunc(Function<? super T, Boolean> func)
    {
        return func::apply;
    }
}

The input functions and predicates are consumers of T, so PECS dictates ? super. You could also add other overloads that could take BooleanSuppliers, Supplier<Boolean>, or any other functional interface type that returns a boolean or Boolean.

This test code compiles. It allows you to pass a variable of a functional interface type and convert it to the desired functional interface type. If you already have the exact functional interface type, you don't have to call the conversion method, but you can if you want.

public class Main
{
    public static void main(String[] args)
    {
        Function<Integer, Boolean> func = x -> true;
        useF(func);
        useF(Convert.toFunc(func));
        useP(Convert.toPred(func));

        Predicate<Integer> pred = x -> true;
        useP(pred);
        useP(Convert.toPred(pred));
        useF(Convert.toFunc(pred));
    }

    static void useF(Function<Integer, Boolean> f) {
        System.out.println("function: " + f.apply(1));
    }
    static void useP(Predicate<Integer> p) {
        System.out.println("predicate: " + p.test(1));
    }
}

The output is:

function: true
function: true
predicate: true
predicate: true
predicate: true
function: true
Sideboard answered 31/3, 2016 at 17:5 Comment(1)
Yes, this is basically what I referred to in this comment. It's possible to convert lambdas by re-wrapping them, but there is no magical Convert.toTargetType method. The problem here is that n*n conversion methods are necessary for n types, although each type already satisfies the requirements for being applicable as the other...Napiform
P
3

Suppose in a shiny future (say Java 10 or 11) we have true function types which allow to specify a function without forcing it to be of a particular conventional Java type (and being some kind of value rather than an object, etc.). Then we still have the issue that the existing methods

static void useF(Function<Integer, Boolean> f) { ... }
static void useP(Predicate<Integer> p) { ... }

expect a Java object implementing a conventional Java interface and behaving like Java objects do, i.e. not suddenly changing the result of theObject instanceof Function or theObject instanceof Predicate. This implies that it will not be the generic function that suddenly starts implementing the required interface when being passed to either of these methods but rather that some kind of capture conversion applies, producing an object implementing the required target interface, much like today, when you pass a lambda expression to either of these methods or when you convert a Predicate to a Function using p::test (or vice versa using f::apply).

So what won’t happen is that you are passing the same object to both methods. You only have an implicit conversion, which will determined at compile-time and likely made explicit in byte code just as with today’s lambda expressions.


A generic method like convertToRequiredTargetType can’t work because it has no knowledge about the target type. The only solutions to make such a thing work are the ones you have precluded, precompilers and byte code manipulation. You could create a method accepting an additional parameter, a Class object describing the require interface, which delegates to the LambdaMetaFactory but that method would have to redo everything the compiler does, determining the functional signature, the name of the method to implement, etc.

For no benefit, as invoking that utility method like convertToRequiredTargetType(theObject) (or actually convertToRequiredTargetType(theObject, Function.class)) is in no way simpler than, e.g. theObject::test). Your desire to create such a method caused your weird statement “Everything seems to be lost immediately after the compilation, where the lambda is hidden in an anonymous object of an anonymous class” when actually, you have an object implementing a functional interface with a known signature and therefore can be converted as simple as function::methodName (where the IDE can complete the method name for you, if you have forgotten)…

Peduncle answered 5/4, 2016 at 17:53 Comment(9)
Of course, there are some technical issues. From a high-level, "language-centered" point of view (in contrast to a VM-centered one), one might wonder why it should not be possible for (x instanceof Function) and Predicate) to return true for one and the same object. So in this regard, I'm not sure whether such an implicit conversion would really be necessary. But sure, one step further, one could ask what x.getClass().getMethods() should return then.... (tbc)Napiform
The question about the "magic conversion method" was rather out of curiosity, whether this is technically possible. For example, regarding the provide method that I sketched, one could (naively!!!) ask whether it's not possible to "pull the type inference into the method" - basically, to determine that the T that is required where this method is called is indeed a functional interface. (I can't give a spot-on reason for this, except for "type inference is frigginly complicated, and there are likely cases where this would not be possible").Napiform
Of course, there is no problem in having an object implementing both interfaces and you can create such thing via lambda expressions even today, just think default methods and intersection types. That choice is still made at compile-time, but could be deferred to creation time. But the point is, you can’t defer it to the time when a particular type is required. Keep in mind that Java allows dynamic loading of classes. There could be a compatible functional interface which hasn’t loaded yet, when the lambda object has been created.Peduncle
As said, the result of x instanceof Function must not suddenly change for the same x due to the appliance of the magic conversion feature. I never said that x instanceof Function and x instanceof Predicate can’t be both true at the same time. That’s possible and rather easy to achieve.Peduncle
I have no idea what your magic convertToRequiredTargetType is aiming at. There is a technical solution capable of doing what you describe, except that it doesn’t work for method invocation, but the much simpler method reference expression. Why should anyone change the Java language just to allow to use an existing feature in a more complicated manner?Peduncle
It's OK. The implicit answer is "It is not possible", and there are some ways to "emulate" a similar behavior (although this may have its own issues ... I'm not sure whether void f(Function f) { p(f); } void p(Predicate p) { f(p); } would be convincing or whether it would provoke a statement that this (particular example in the given form) does not make sense.Napiform
You can already write <T> void f(Function<T,Boolean> f) { p(f::apply); } <T> void p(Predicate<T> p) { f(p::test); } today, but of course, if you do it exactly this way, you’ll likely get a StackOverflowError… Note that this feature doesn’t even require the function to be implemented as a lambda expression.Peduncle
Sure, this "recursion" should point out that (at a recursion depth of 10) you will have something where p.test(i) is equivalent to calling 10 alternatingly nested functions (f0(x) = { return p0(x); }, p0(x) = { return f1(x); }, ..., p10(x) = { return f10(x); }, f10(x) = { true }, ). It is a true wrapping that takes place there.Napiform
That’s an implementation detail. In principle, a JRE is allowed to detect such a wrapped function and unwrap it instead of wrapping it again. It wouldn’t be too hard to implement…Peduncle
M
1

I'm late to this party, but I just had a similar problem, and here's the solution I came up with. It will convert any functional interface to any other with matching parameters and return type. It uses a proxy, which may or may not be appropriate. It performs no unsafe casts, but it will throw if any of the interfaces is not functional or if they don't match.

  public static <S, T> T convertFunctional(S source, Class<S> sourceClass, Class<T> targetClass) {
    var sourceAbstractMethod = getOnlyAbstractMethod(sourceClass);
    var targetAbstractMethod = getOnlyAbstractMethod(targetClass);
    return (T) Proxy.newProxyInstance(targetClass.getClassLoader(), new Class<?>[] {targetClass},
            (proxy, method, args) -> handleProxyInvocation(proxy, method, args, source, sourceAbstractMethod,
                    targetAbstractMethod));
  }

  private static Method getOnlyAbstractMethod(Class<?> aClass) {
    var methods = Arrays.stream(aClass.getDeclaredMethods())
                        .filter(method -> Modifier.isAbstract(method.getModifiers())).toList();
    if (methods.size() != 1) {
      throw new IllegalArgumentException(String.format(
              "Not a functional interface - doesn't have exactly one abstract method: %s", aClass.getName()));
    }

    return methods.get(0);
  }

  private static Object handleProxyInvocation(Object proxy, Method method, Object[] args, Object source,
                                                  Method sourceAbstractMethod, Method targetAbstractMethod) throws Throwable {
    if (method.equals(targetAbstractMethod)) {
      // This will fail if arg count or types don't match
      return sourceAbstractMethod.invoke(source, args);
    }

    if (method.isDefault()) {
      // For default methods declared on the target interface
      return InvocationHandler.invokeDefault(proxy, method, args);
    }

    // For Object methods
    return method.invoke(source, args);
  }
Mummer answered 25/6 at 23:11 Comment(1)
It is necessary to pass in the source- and target class, so that's not exactly it, but ... using a proxy is much more generic than the other suggestions, so it's definitely a +1 as well.Napiform

© 2022 - 2024 — McMap. All rights reserved.