web-dev-qa-db-fra.com

Pourquoi cette inférence de type ne fonctionne-t-elle pas avec ce scénario d'expression Lambda?

J'ai un scénario étrange où l'inférence de type ne fonctionne pas comme je m'y attendais lors de l'utilisation d'une expression lambda. Voici une approximation de mon scénario réel:

static class Value<T> {
}

@FunctionalInterface
interface Bar<T> {
  T apply(Value<T> value); // Change here resolves error
}

static class Foo {
  public static <T> T foo(Bar<T> callback) {
  }
}

void test() {
  Foo.foo(value -> true).booleanValue(); // Compile error here
}

L'erreur de compilation que j'obtiens sur l'avant-dernière ligne est

La méthode booleanValue () n'est pas définie pour le type Object

si je lance le lambda sur Bar<Boolean>:

Foo.foo((Bar<Boolean>)value -> true).booleanValue();

ou si je change la signature de méthode de Bar.apply pour utiliser des types bruts:

T apply(Value value);

alors le problème disparaît. La façon dont je m'attendrais à ce que cela fonctionne est que:

  • Foo.foo l'appel devrait déduire un type de retour de boolean
  • value dans le lambda doit être déduit de Value<Boolean>.

Pourquoi cette inférence ne fonctionne-t-elle pas comme prévu et comment puis-je modifier cette API pour qu'elle fonctionne comme prévu?

38
Josh Stone

Sous la capuche

En utilisant certaines fonctionnalités cachées de javac, nous pouvons obtenir plus d'informations sur ce qui se passe:

$ javac -XDverboseResolution=deferred-inference,success,applicable LambdaInference.Java 
LambdaInference.Java:16: Note: resolving method foo in type Foo to candidate 0
    Foo.foo(value -> true).booleanValue(); // Compile error here
       ^
  phase: BASIC
  with actuals: <none>
  with type-args: no arguments
  candidates:
      #0 applicable method found: <T>foo(Bar<T>)
        (partially instantiated to: (Bar<Object>)Object)
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.Java:16: Note: Deferred instantiation of method <T>foo(Bar<T>)
    Foo.foo(value -> true).booleanValue(); // Compile error here
           ^
  instantiated signature: (Bar<Object>)Object
  target-type: <none>
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.Java:16: error: cannot find symbol
    Foo.foo(value -> true).booleanValue(); // Compile error here
                          ^
  symbol:   method booleanValue()
  location: class Object
1 error

C'est beaucoup d'informations, décomposons-les.

LambdaInference.Java:16: Note: resolving method foo in type Foo to candidate 0
    Foo.foo(value -> true).booleanValue(); // Compile error here
       ^
  phase: BASIC
  with actuals: <none>
  with type-args: no arguments
  candidates:
      #0 applicable method found: <T>foo(Bar<T>)
        (partially instantiated to: (Bar<Object>)Object)
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)

phase: phase d'applicabilité de la méthode
réels: les arguments réels passés dans
type-args: arguments de type explicites
candidats: méthodes potentiellement applicables

réel est <none> parce que notre lambda typé implicitement n'est pas pertinent pour l'applicabilité .

Le compilateur résout votre appel de foo à la seule méthode nommée foo dans Foo. Il a été partiellement instancié en Foo.<Object> foo (Car il n'y avait pas de réels ou d'arguments de type), mais cela peut changer au stade de l'inférence différée.

LambdaInference.Java:16: Note: Deferred instantiation of method <T>foo(Bar<T>)
    Foo.foo(value -> true).booleanValue(); // Compile error here
           ^
  instantiated signature: (Bar<Object>)Object
  target-type: <none>
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)

signature instanciée: la signature entièrement instanciée de foo. C'est le résultat de cette étape (à ce stade, plus aucune inférence de type ne sera faite sur la signature de foo).
type-cible: le contexte dans lequel l'appel est effectué. Si l'invocation de la méthode fait partie d'une affectation, elle sera du côté gauche. Si l'appel de méthode fait lui-même partie d'un appel de méthode, ce sera le type de paramètre.

Étant donné que votre appel de méthode est suspendu, il n'y a pas de type cible. Puisqu'il n'y a pas de type cible, plus aucune inférence ne peut être faite sur foo et T est supposé être Object.


Une analyse

Le compilateur n'utilise pas de lambdas implicitement typés lors de l'inférence. Dans une certaine mesure, cela a du sens. En général, étant donné param -> BODY, Vous ne pourrez pas compiler BODY tant que vous n'aurez pas de type pour param. Si vous essayez de déduire le type de param de BODY, cela peut entraîner un problème de type poulet et œuf. Il est possible que certaines améliorations soient apportées à ce sujet dans les futures versions de Java.


Solutions

Foo.<Boolean> foo(value -> true)

Cette solution fournit un argument de type explicite à foo (notez la section with type-args Ci-dessous). Cela change l'instanciation partielle de la signature de la méthode en (Bar<Boolean>)Boolean, Ce que vous voulez.

LambdaInference.Java:16: Note: resolving method foo in type Foo to candidate 0
    Foo.<Boolean> foo(value -> true).booleanValue(); // Compile error here
       ^
  phase: BASIC
  with actuals: <none>
  with type-args: Boolean
  candidates:
      #0 applicable method found: <T>foo(Bar<T>)
        (partially instantiated to: (Bar<Boolean>)Boolean)
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.Java:16: Note: resolving method booleanValue in type Boolean to candidate 0
    Foo.<Boolean> foo(value -> true).booleanValue(); // Compile error here
                                    ^
  phase: BASIC
  with actuals: no arguments
  with type-args: no arguments
  candidates:
      #0 applicable method found: booleanValue()

Foo.foo((Value<Boolean> value) -> true)

Cette solution tape explicitement votre lambda, ce qui lui permet d'être pertinent pour l'applicabilité (notez with actuals Ci-dessous). Cela change l'instanciation partielle de la signature de la méthode en (Bar<Boolean>)Boolean, Ce que vous voulez.

LambdaInference.Java:16: Note: resolving method foo in type Foo to candidate 0
    Foo.foo((Value<Boolean> value) -> true).booleanValue(); // Compile error here
       ^
  phase: BASIC
  with actuals: Bar<Boolean>
  with type-args: no arguments
  candidates:
      #0 applicable method found: <T>foo(Bar<T>)
        (partially instantiated to: (Bar<Boolean>)Boolean)
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.Java:16: Note: Deferred instantiation of method <T>foo(Bar<T>)
    Foo.foo((Value<Boolean> value) -> true).booleanValue(); // Compile error here
           ^
  instantiated signature: (Bar<Boolean>)Boolean
  target-type: <none>
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.Java:16: Note: resolving method booleanValue in type Boolean to candidate 0
    Foo.foo((Value<Boolean> value) -> true).booleanValue(); // Compile error here
                                           ^
  phase: BASIC
  with actuals: no arguments
  with type-args: no arguments
  candidates:
      #0 applicable method found: booleanValue()

Foo.foo((Bar<Boolean>) value -> true)

Comme ci-dessus, mais avec une saveur légèrement différente.

LambdaInference.Java:16: Note: resolving method foo in type Foo to candidate 0
    Foo.foo((Bar<Boolean>) value -> true).booleanValue(); // Compile error here
       ^
  phase: BASIC
  with actuals: Bar<Boolean>
  with type-args: no arguments
  candidates:
      #0 applicable method found: <T>foo(Bar<T>)
        (partially instantiated to: (Bar<Boolean>)Boolean)
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.Java:16: Note: Deferred instantiation of method <T>foo(Bar<T>)
    Foo.foo((Bar<Boolean>) value -> true).booleanValue(); // Compile error here
           ^
  instantiated signature: (Bar<Boolean>)Boolean
  target-type: <none>
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.Java:16: Note: resolving method booleanValue in type Boolean to candidate 0
    Foo.foo((Bar<Boolean>) value -> true).booleanValue(); // Compile error here
                                         ^
  phase: BASIC
  with actuals: no arguments
  with type-args: no arguments
  candidates:
      #0 applicable method found: booleanValue()

Boolean b = Foo.foo(value -> true)

Cette solution fournit une cible explicite pour votre appel de méthode (voir target-type Ci-dessous). Cela permet à l'instanciation différée de déduire que le paramètre type doit être Boolean au lieu de Object (voir instantiated signature Ci-dessous).

LambdaInference.Java:16: Note: resolving method foo in type Foo to candidate 0
    Boolean b = Foo.foo(value -> true);
                   ^
  phase: BASIC
  with actuals: <none>
  with type-args: no arguments
  candidates:
      #0 applicable method found: <T>foo(Bar<T>)
        (partially instantiated to: (Bar<Object>)Object)
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.Java:16: Note: Deferred instantiation of method <T>foo(Bar<T>)
    Boolean b = Foo.foo(value -> true);
                       ^
  instantiated signature: (Bar<Boolean>)Boolean
  target-type: Boolean
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)

Avertissement

C'est le comportement qui se produit. Je ne sais pas si c'est ce qui est spécifié dans le JLS. Je pourrais fouiller et voir si je pouvais trouver la section exacte qui spécifie ce comportement, mais la notation inférence de type me donne mal à la tête.

Cela n'explique pas non plus complètement pourquoi la modification de Bar pour utiliser un Value brut résoudrait ce problème:

LambdaInference.Java:16: Note: resolving method foo in type Foo to candidate 0
    Foo.foo(value -> true).booleanValue();
       ^
  phase: BASIC
  with actuals: <none>
  with type-args: no arguments
  candidates:
      #0 applicable method found: <T>foo(Bar<T>)
        (partially instantiated to: (Bar<Object>)Object)
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.Java:16: Note: Deferred instantiation of method <T>foo(Bar<T>)
    Foo.foo(value -> true).booleanValue();
           ^
  instantiated signature: (Bar<Boolean>)Boolean
  target-type: <none>
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.Java:16: Note: resolving method booleanValue in type Boolean to candidate 0
    Foo.foo(value -> true).booleanValue();
                          ^
  phase: BASIC
  with actuals: no arguments
  with type-args: no arguments
  candidates:
      #0 applicable method found: booleanValue()

Pour une raison quelconque, le changer pour utiliser un Value brut permet à l'instanciation différée de déduire que T est Boolean. Si je devais spéculer, je suppose que lorsque le compilateur tente d'adapter le lambda au Bar<T>, Il peut déduire que T est Boolean en regardant le corps de le lambda. Cela implique que mon analyse antérieure est incorrecte. Le compilateur peut effectuer l'inférence de type sur le corps d'un lambda, mais uniquement sur des variables de type qui seulement apparaissent dans le type de retour.

31
Jeffrey

L'inférence sur le type de paramètre lambda ne peut pas dépendre du corps lambda.

Le compilateur fait face à un travail difficile en essayant de donner un sens aux expressions lambda implicites

    foo( value -> GIBBERISH )

Le type de value doit d'abord être déduit avant que GIBBERISH puisse être compilé, car en général l'interprétation de GIBBERISH dépend de la définition de value.

(Dans votre cas particulier, GIBBERISH se trouve être une simple constante indépendante de value.)

Javac doit déduire Value<T> d'abord pour le paramètre value; il n'y a pas de contraintes dans le contexte, donc T=Object. Ensuite, le corps lambda true est compilé et reconnu comme booléen, compatible avec T.

Après avoir apporté la modification à l'interface fonctionnelle, le type de paramètre lambda ne nécessite pas d'inférence; T reste sans interférence. Ensuite, le corps lambda est compilé et le type de retour semble être booléen, qui est défini comme une limite inférieure pour T.


Un autre exemple démontrant le problème

<T> void foo(T v, Function<T,T> f) { ... }

foo("", v->42);  // Error. why can't javac infer T=Object ?

T est supposé être String; le corps de lambda n'a pas participé à l'inférence.

Dans cet exemple, le comportement de javac nous semble très raisonnable; cela a probablement empêché une erreur de programmation. Vous ne voulez pas que l'inférence soit trop puissante; si tout ce que nous écrivons se compile d'une manière ou d'une autre, nous perdrons confiance pour que le compilateur trouve des erreurs pour nous.


Il existe d'autres exemples où le corps lambda semble fournir des contraintes sans équivoque, mais le compilateur ne peut pas utiliser ces informations. En Java, les types de paramètres lambda doivent être fixés en premier, avant que le corps puisse être consulté. Il s'agit d'une décision délibérée. En revanche, C # est prêt à essayer différents types de paramètres et à voir ce qui rend le code compile. Java considère cela trop risqué.

Dans tous les cas, lorsque lambda implicite échoue, ce qui arrive assez fréquemment, fournissez des types explicites pour les paramètres lambda; dans ton cas, (Value<Boolean> value)->true

5
ZhongYu

Le moyen facile de résoudre ce problème est une déclaration de type sur l'appel de méthode à foo:

Foo.<Boolean>foo(value -> true).booleanValue();

Edit: Je ne trouve pas la documentation spécifique expliquant pourquoi cela est nécessaire, à peu près comme tout le monde. Je soupçonnais que c'était peut-être à cause des types primitifs, mais ce n'était pas vrai. Quoi qu'il en soit, cette syntaxe est appelée à l'aide d'un Type cible . Aussi Type cible dans Lambdas . Les raisons m'échappent cependant, je ne trouve nulle part de documentation sur la raison pour laquelle ce cas d'utilisation particulier est nécessaire.

Edit 2: J'ai trouvé cette question pertinente:

L'inférence de type générique ne fonctionne pas avec le chaînage de méthode?

Il semble que ce soit parce que vous enchaînez les méthodes ici. Selon les commentaires JSR référencés dans la réponse acceptée, il s'agissait d'une omission délibérée de fonctionnalités, car le compilateur n'a aucun moyen de transmettre des informations de type générique déduites entre les appels de méthode chaînés dans les deux directions. En conséquence, le type entier de effacé par le temps, il arrive à l'appel à booleanValue. L'ajout du type cible dans supprime ce comportement en fournissant la contrainte manuellement au lieu de laisser le compilateur prendre la décision en utilisant les règles décrites dans JLS §18 , ce qui ne semble pas du tout le mentionner. C'est la seule information que j'ai pu trouver. Si quelqu'un trouve quelque chose de mieux, j'adorerais le voir.

4
Brian

Comme les autres réponses, j'espère également que quelqu'un de plus intelligent pourra montrer pourquoi le compilateur n'est pas en mesure de déduire que T est Boolean.

Une façon d'aider le compilateur à faire la bonne chose, sans nécessiter de modification de la conception de votre classe/interface existante, consiste à déclarer explicitement le type du paramètre formel dans votre expression lambda. Donc, dans ce cas, en déclarant explicitement que le type du paramètre value est Value<Boolean>.

void test() {
  Foo.foo((Value<Boolean> value) -> true).booleanValue();
}
3
sstan

Problème

La valeur sera déduite du type Value<Object> parce que vous avez mal interprété le lambda. Pensez-y, comme vous appelez directement avec le lambda la méthode apply. Donc ce que vous faites c'est:

Boolean apply(Value value);

et cela est correctement déduit de:

Boolean apply(Value<Object> value);

puisque vous n'avez pas donné le type pour Value.

Solution simple

Appelez le lambda de manière correcte:

Foo.foo((Value<Boolean> value) -> true).booleanValue();

cela sera déduit de:

Boolean apply(Value<Boolean> value);

(Ma) Solution recommandée

Votre solution devrait être un peu plus claire. Si vous souhaitez un rappel, vous avez besoin d'une valeur de type qui sera retournée.

J'ai créé une interface de rappel générique, une classe de valeur générique et une classe d'utilisation pour montrer comment l'utiliser.

Interface de rappel

/**
 *
 * @param <P> The parameter to call
 * @param <R> The return value you get
 */
@FunctionalInterface
public interface Callback<P, R> {

  public R call(P param);
}

Classe de valeur

public class Value<T> {

  private final T field;

  public Value(T field) {
    this.field = field;
  }

  public T getField() {
    return field;
  }
}

Classe UsingClass

public class UsingClass<T> {

  public T foo(Callback<Value<T>, T> callback, Value<T> value) {
    return callback.call(value);
  }
}

TestApp avec main

public class TestApp {

  public static void main(String[] args) {
    Value<Boolean> boolVal = new Value<>(false);
    Value<String> stringVal = new Value<>("false");

    Callback<Value<Boolean>, Boolean> boolCb = (v) -> v.getField();
    Callback<Value<String>, String> stringCb = (v) -> v.getField();

    UsingClass<Boolean> usingClass = new UsingClass<>();
    boolean val = usingClass.foo(boolCb, boolVal);
    System.out.println("Boolean value: " + val);

    UsingClass<String> usingClass1 = new UsingClass<>();
    String val1 = usingClass1.foo(stringCb, stringVal);
    System.out.println("String value: " + val1);

    // this will give you a clear and understandable compiler error
    //boolean val = usingClass.foo(boolCb, stringVal);
  }
}
1
aw-think

Je ne sais pas pourquoi mais vous devez ajouter un type de retour distinct:

public class HelloWorld{
static class Value<T> {
}

@FunctionalInterface
interface Bar<T,R> {
      R apply(Value<T> value); // Return type added
}

static class Foo {
  public static <T,R> R foo(Bar<T,R> callback) {
      return callback.apply(new Value<T>());
  }
}

void test() {
  System.out.println( Foo.foo(value -> true).booleanValue() ); // No compile error here
}
     public static void main(String []args){
         new HelloWorld().test();
     }
}

un gars intelligent peut probablement expliquer cela.

1
fukanchik