¿Por qué un parámetro de tipo es más fuerte que un parámetro de método?

12

Por que es

public <R, F extends Function<T, R>> Builder<T> withX(F getter, R returnValue) {...}

más estricto entonces

public <R> Builder<T> with(Function<T, R> getter, R returnValue) {...}

Este es un seguimiento de por qué el tipo de retorno lambda no está marcado en tiempo de compilación . Encontré usando el métodowithX() como

.withX(MyInterface::getLength, "I am not a Long")

produce el error de tiempo de compilación deseado:

El tipo de getLength () del tipo BuilderExample.MyInterface es largo, esto es incompatible con el tipo de retorno del descriptor: String

mientras usa el método with() no.

ejemplo completo:

import java.util.function.Function;

public class SO58376589 {
  public static class Builder<T> {
    public <R, F extends Function<T, R>> Builder<T> withX(F getter, R returnValue) {
      return this;
    }

    public <R> Builder<T> with(Function<T, R> getter, R returnValue) {
      return this;
    }

  }

  static interface MyInterface {
    public Long getLength();
  }

  public static void main(String[] args) {
    Builder<MyInterface> b = new Builder<MyInterface>();
    Function<MyInterface, Long> getter = MyInterface::getLength;
    b.with(getter, 2L);
    b.with(MyInterface::getLength, 2L);
    b.withX(getter, 2L);
    b.withX(MyInterface::getLength, 2L);
    b.with(getter, "No NUMBER"); // error
    b.with(MyInterface::getLength, "No NUMBER"); // NO ERROR !!
    b.withX(getter, "No NUMBER"); // error
    b.withX(MyInterface::getLength, "No NUMBER"); // error !!!
  }
}

javac SO58376589.java

SO58376589.java:32: error: method with in class Builder<T> cannot be applied to given types;
    b.with(getter, "No NUMBER"); // error
     ^
  required: Function<MyInterface,R>,R
  found: Function<MyInterface,Long>,String
  reason: inference variable R has incompatible bounds
    equality constraints: Long
    lower bounds: String
  where R,T are type-variables:
    R extends Object declared in method <R>with(Function<T,R>,R)
    T extends Object declared in class Builder
SO58376589.java:34: error: method withX in class Builder<T> cannot be applied to given types;
    b.withX(getter, "No NUMBER"); // error
     ^
  required: F,R
  found: Function<MyInterface,Long>,String
  reason: inference variable R has incompatible bounds
    equality constraints: Long
    lower bounds: String
  where F,R,T are type-variables:
    F extends Function<MyInterface,R> declared in method <R,F>withX(F,R)
    R extends Object declared in method <R,F>withX(F,R)
    T extends Object declared in class Builder
SO58376589.java:35: error: incompatible types: cannot infer type-variable(s) R,F
    b.withX(MyInterface::getLength, "No NUMBER"); // error
           ^
    (argument mismatch; bad return type in method reference
      Long cannot be converted to String)
  where R,F,T are type-variables:
    R extends Object declared in method <R,F>withX(F,R)
    F extends Function<T,R> declared in method <R,F>withX(F,R)
    T extends Object declared in class Builder
3 errors

Ejemplo extendido

El siguiente ejemplo muestra el comportamiento diferente del método y el parámetro de tipo reducido a un Proveedor. Además, muestra la diferencia con el comportamiento del consumidor para un parámetro de tipo. Y muestra que no hace una diferencia si es un Consumidor o Proveedor para un parámetro de método.

import java.util.function.Consumer;
import java.util.function.Supplier;
interface TypeInference {

  Number getNumber();

  void setNumber(Number n);

  @FunctionalInterface
  interface Method<R> {
    TypeInference be(R r);
  }

  //Supplier:
  <R> R letBe(Supplier<R> supplier, R value);
  <R, F extends Supplier<R>> R letBeX(F supplier, R value);
  <R> Method<R> let(Supplier<R> supplier);  // return (x) -> this;

  //Consumer:
  <R> R lettBe(Consumer<R> supplier, R value);
  <R, F extends Consumer<R>> R lettBeX(F supplier, R value);
  <R> Method<R> lett(Consumer<R> consumer);


  public static void main(TypeInference t) {
    t.letBe(t::getNumber, (Number) 2); // Compiles :-)
    t.lettBe(t::setNumber, (Number) 2); // Compiles :-)
    t.letBe(t::getNumber, 2); // Compiles :-)
    t.lettBe(t::setNumber, 2); // Compiles :-)
    t.letBe(t::getNumber, "NaN"); // !!!! Compiles :-(
    t.lettBe(t::setNumber, "NaN"); // Does not compile :-)

    t.letBeX(t::getNumber, (Number) 2); // Compiles :-)
    t.lettBeX(t::setNumber, (Number) 2); // Compiles :-)
    t.letBeX(t::getNumber, 2); // !!! Does not compile  :-(
    t.lettBeX(t::setNumber, 2); // Compiles :-)
    t.letBeX(t::getNumber, "NaN"); // Does not compile :-)
    t.lettBeX(t::setNumber, "NaN"); // Does not compile :-)

    t.let(t::getNumber).be(2); // Compiles :-)
    t.lett(t::setNumber).be(2); // Compiles :-)
    t.let(t::getNumber).be("NaN"); // Does not compile :-)
    t.lett(t::setNumber).be("NaN"); // Does not compile :-)
  }
}
jukzi
fuente
1
Debido a la inferencia con este último. Aunque ambos están basados ​​en el caso de uso que uno necesita implementar. Para los suyos, el primero podría ser estricto y bueno. Por flexibilidad, alguien más puede preferir lo último.
Naman
¿Estás intentando compilar esto en Eclipse? La búsqueda de cadenas de error del formato que pegó sugiere que este es un error específico de Eclipse (ecj). ¿ javacTiene el mismo problema al compilar con herramientas sin formato o de compilación como Gradle o Maven?
user31601
@ user31601 agregué un ejemplo completo con salida de javac. Los mensajes de error tienen un formato poco diferente pero aún eclipse y javac tienen el mismo comportamiento
jukzi

Respuestas:

12

Esta es una pregunta realmente interesante. La respuesta, me temo, es complicada.

tl; dr

Resolver la diferencia implica una lectura bastante profunda de la especificación de inferencia de tipos de Java , pero básicamente se reduce a esto:

  • Todas las demás cosas iguales, el compilador infiere el tipo más específico que puede.
  • Sin embargo, si puede encontrar una sustitución para un parámetro de tipo que satisfaga todos los requisitos, la compilación tendrá éxito. vaga que resulte la sustitución.
  • Porque withhay una sustitución (ciertamente vaga) que satisface todos los requisitos sobre R:Serializable
  • Para withX, la introducción del parámetro de tipo adicional Fobliga al compilador a resolver Rprimero, sin considerar la restricción F extends Function<T,R>. Rse resuelve en (mucho más específico), lo Stringque significa que la inferencia de Ffalla.

Este último punto es el más importante, pero también el más ondulado a mano. No puedo pensar en una mejor forma concisa de redacción, así que si quieres más detalles, te sugiero que leas la explicación completa a continuación.

¿Es este el comportamiento previsto?

Voy a arriesgarme aquí y decir que no .

No estoy sugiriendo que haya un error en la especificación, más que (en el caso de withX) los diseñadores de idiomas han levantado la mano y han dicho "hay algunas situaciones en las que la inferencia de tipos se vuelve demasiado difícil, así que simplemente fallaremos" . Aunque el comportamiento del compilador con respecto awithX parece ser lo que desea, consideraría que es un efecto secundario incidental de la especificación actual, en lugar de una decisión de diseño intencionadamente positiva.

Esto es importante porque informa la pregunta ¿Debo confiar en este comportamiento en el diseño de mi aplicación? Yo diría que no deberías, porque no puedes garantizar que futuras versiones del lenguaje continuarán comportándose de esta manera.

Si bien es cierto que los diseñadores de idiomas se esfuerzan mucho por no romper las aplicaciones existentes cuando actualizan sus especificaciones / diseño / compilador, el problema es que el comportamiento en el que desea confiar es aquel en el que el compilador falla actualmente (es decir, no es una aplicación existente ). Las actualizaciones de Langauge convierten el código que no se compila en código de compilación todo el tiempo. Por ejemplo, se podría garantizar que el siguiente código no se compilará en Java 7, pero en Java 8:

static Runnable x = () -> System.out.println();

Su caso de uso no es diferente.

Otra razón por la que sería cauteloso al usar su withXmétodo es el Fparámetro en sí. Generalmente, existe un parámetro de tipo genérico en un método (que no aparece en el tipo de retorno) para unir los tipos de varias partes de la firma. Está diciendo:

No me importa lo que Tsea, pero quiero estar seguro de que donde sea que lo use Tes del mismo tipo.

Lógicamente, entonces, esperaríamos que cada parámetro de tipo aparezca al menos dos veces en la firma de un método, de lo contrario, "no está haciendo nada". Fen su withXsolo aparece una vez en la firma, lo que me sugiere el uso de un parámetro de tipo que no está en línea con la intención de esta característica del lenguaje.

Una implementación alternativa

Una forma de implementar esto de una manera un poco más de "comportamiento previsto" sería dividir su withmétodo en una cadena de 2:

public class Builder<T> {

    public final class With<R> {
        private final Function<T,R> method;

        private With(Function<T,R> method) {
            this.method = method;
        }

        public Builder<T> of(R value) {
            // TODO: Body of your old 'with' method goes here
            return Builder.this;
        }
    }

    public <R> With<R> with(Function<T,R> method) {
        return new With<>(method);
    }

}

Esto se puede usar de la siguiente manera:

b.with(MyInterface::getLong).of(1L); // Compiles
b.with(MyInterface::getLong).of("Not a long"); // Compiler error

Esto no incluye un parámetro de tipo extraño como lo withXhace. Al dividir el método en dos firmas, también expresa mejor la intención de lo que está tratando de hacer, desde un punto de vista de seguridad de tipo:

  • El primer método configura una clase ( With) que define el tipo en función de la referencia del método.
  • El segundo método ( of) restringe el tipo de valuecompatibilidad con lo que configuró anteriormente.

La única forma en que una versión futura del lenguaje podría compilar esto es si se implementa el tipeo completo, lo que parece poco probable.

Una nota final para hacer que todo esto sea irrelevante: creo que Mockito (y en particular su funcionalidad de copia de seguridad) básicamente ya podría hacer lo que está tratando de lograr con su "generador de tipos genéricos seguros". ¿Tal vez podrías usar eso en su lugar?

La explicación completa (ish)

Voy a trabajar a través del procedimiento de inferencia de tipos para ambos withy withX. Esto es bastante largo, así que tómalo con calma. A pesar de ser largo, todavía he dejado bastantes detalles. Es posible que desee consultar la especificación para obtener más detalles (siga los enlaces) para convencerse de que tengo razón (es posible que haya cometido un error).

Además, para simplificar un poco las cosas, voy a usar una muestra de código más mínima. La principal diferencia es que se intercambia, Functionpor Supplierlo que hay menos tipos y parámetros en juego. Aquí hay un fragmento completo que reproduce el comportamiento que describió:

public class TypeInference {

    static long getLong() { return 1L; }

    static <R> void with(Supplier<R> supplier, R value) {}
    static <R, F extends Supplier<R>> void withX(F supplier, R value) {}

    public static void main(String[] args) {
        with(TypeInference::getLong, "Not a long");       // Compiles
        withX(TypeInference::getLong, "Also not a long"); // Does not compile
    }

}

Analicemos la inferencia de aplicabilidad de tipo y el procedimiento de inferencia de tipo para cada invocación de método:

with

Tenemos:

with(TypeInference::getLong, "Not a long");

El conjunto límite inicial, B 0 , es:

  • R <: Object

Todas las expresiones de parámetros son pertinentes a la aplicabilidad .

Por lo tanto, la restricción inicial establecida para la inferencia de aplicabilidad , C , es:

  • TypeInference::getLong es compatible con Supplier<R>
  • "Not a long" es compatible con R

Esto se reduce al conjunto de enlaces B 2 de:

  • R <: Object(de B 0 )
  • Long <: R (desde la primera restricción)
  • String <: R (de la segunda restricción)

Dado que esto no contiene el límite ' falso ', y (supongo) la resolución de Réxitos (dar Serializable), entonces la invocación es aplicable.

Entonces, pasamos a la inferencia de tipo de invocación .

El nuevo conjunto de restricciones, C , con variables de entrada y salida asociadas , es:

  • TypeInference::getLong es compatible con Supplier<R>
    • Variables de entrada: ninguna
    • Variables de salida: R

Esto no contiene interdependencias entre las variables de entrada y salida , por lo que puede reducirse en un solo paso, y el conjunto de límite final, B 4 , es el mismo que B 2 . Por lo tanto, la resolución tiene éxito como antes, ¡y el compilador da un suspiro de alivio!

withX

Tenemos:

withX(TypeInference::getLong, "Also not a long");

El conjunto límite inicial, B 0 , es:

  • R <: Object
  • F <: Supplier<R>

Solo la expresión del segundo parámetro es pertinente a la aplicabilidad . El primero ( TypeInference::getLong) no lo es, porque cumple con la siguiente condición:

Si mes un método genérico y la invocación del método no proporciona argumentos de tipo explícito, una expresión lambda tipada explícitamente o una expresión de referencia de método exacta para la cual el tipo de destino correspondiente (derivado de la firma de m) es un parámetro de tipo m.

Por lo tanto, la restricción inicial establecida para la inferencia de aplicabilidad , C , es:

  • "Also not a long" es compatible con R

Esto se reduce al conjunto de enlaces B 2 de:

  • R <: Object(de B 0 )
  • F <: Supplier<R>(de B 0 )
  • String <: R (de la restricción)

Nuevamente, dado que esto no contiene el límite ' falso ' y la resolución de los Réxitos (donaciones String), entonces la invocación es aplicable.

Inferencia de tipo de invocación una vez más ...

Esta vez, el nuevo conjunto de restricciones, C , con variables de entrada y salida asociadas , es:

  • TypeInference::getLong es compatible con F
    • Variables de entrada: F
    • Variables de salida: ninguna

Nuevamente, no tenemos interdependencias entre las variables de entrada y salida . Sin embargo esta vez, no es una variable de entrada ( F), por lo que hay que resolver esto antes de intentar la reducción . Entonces, comenzamos con nuestro conjunto enlazado B 2 .

  1. Determinamos un subconjunto de la Vsiguiente manera:

    Dado un conjunto de variables de inferencia para resolver, Vsea ​​la unión de este conjunto y todas las variables de las que depende la resolución de al menos una variable en este conjunto.

    Por el segundo límite en B 2 , la resolución de Fdepende de R, entonces V := {F, R}.

  2. Elegimos un subconjunto de Vacuerdo con la regla:

    dejemos que { α1, ..., αn }sea ​​un subconjunto no vacío de variables no desinstaladas de Vtal manera que i) para todos i (1 ≤ i ≤ n), si αidepende de la resolución de una variable β, entonces βtiene una instanciación o hay algo jasí β = αj; y ii) no existe un subconjunto adecuado no vacío de { α1, ..., αn }esta propiedad.

    El único subconjunto de Veso satisface esta propiedad es {R}.

  3. Usando el tercer límite ( String <: R), instanciamos R = Stringe incorporamos esto a nuestro conjunto de límites. Rahora está resuelto, y el segundo límite se convierte efectivamente F <: Supplier<String>.

  4. Usando el segundo límite (revisado), instanciamos F = Supplier<String>. Fahora está resuelto.

Ahora que Festá resuelto, podemos proceder con la reducción , utilizando la nueva restricción:

  1. TypeInference::getLong es compatible con Supplier<String>
  2. ... reduce a Long es compatible con String
  3. ... que se reduce a falso

... y tenemos un error de compilación!


Notas adicionales sobre el 'Ejemplo extendido'

El ejemplo extendido en la pregunta analiza algunos casos interesantes que no están cubiertos directamente por el funcionamiento anterior:

  • Donde el tipo de valor es un subtipo del método return type ( Integer <: Number)
  • Donde la interfaz funcional es contravariante en el tipo inferido (es decir, en Consumerlugar de Supplier)

En particular, 3 de las invocaciones dadas se destacan por sugerir potencialmente un comportamiento de compilador 'diferente' al descrito en las explicaciones:

t.lettBe(t::setNumber, "NaN"); // Does not compile :-)

t.letBeX(t::getNumber, 2); // !!! Does not compile  :-(
t.lettBeX(t::setNumber, 2); // Compiles :-)

El segundo de estos 3 pasará exactamente por el mismo proceso de inferencia que el withXanterior (solo reemplace Longcon Numbery Stringcon Integer). Esto ilustra otra razón más por la que no debe confiar en este comportamiento de inferencia de tipo fallido para el diseño de su clase, ya que la falla al compilar aquí probablemente no sea un comportamiento deseable.

Para los otros 2 (y, de hecho, para cualquiera de las otras invocaciones que involucran un proceso por el Consumerque desea trabajar), el comportamiento debería ser evidente si trabaja a través del procedimiento de inferencia de tipos establecido para uno de los métodos anteriores (es decir, withpara el primero, withXpara el tercero). Solo hay un pequeño cambio que debe tener en cuenta:

  • La restricción en el primer parámetro ( t::setNumber es compatible con Consumer<R> ) se reducirá en R <: Numberlugar de Number <: Rcomo lo hace para Supplier<R>. Esto se describe en la documentación vinculada sobre la reducción.

Lo dejo como un ejercicio para que el lector trabaje cuidadosamente a través de uno de los procedimientos anteriores, armado con este conocimiento adicional, para demostrarse a sí mismo exactamente por qué una invocación particular se compila o no.

usuario31601
fuente
Muy en profundidad, bien investigado y formulado. ¡Gracias!
Zabuzard el
@ user31601 ¿Puede indicar dónde entra en juego la diferencia de proveedor a consumidor? Agregué un ejemplo extendido en la pregunta original para eso. Muestra el comportamiento covariante, contravariante e invariante para las diferentes versiones de letBe (), letBeX () y let (). Be () dependiendo del proveedor / consumidor.
jukzi
@jukzi He agregado algunas notas adicionales, pero debe tener suficiente información para trabajar con estos nuevos ejemplos usted mismo.
user31601
Eso es interesante: tantos casos especiales en 18.2.1. para lambdas y referencias de métodos donde no hubiera esperado ningún caso especial para ellos desde mi comprensión ingenua. Y probablemente ningún desarrollador ordinario esperaría.
jukzi
Bueno, supongo que la razón es que con las lambdas y las referencias de métodos, el compilador debe decidir qué tipo adecuado debe implementar la lambda, ¡tiene que elegir! Por ejemplo, TypeInference::getLongpodría imlement Supplier<Long>or Supplier<Serializable>or Supplier<Number>etc, ¡pero crucialmente solo puede implementar uno de ellos (como cualquier otra clase)! Esto es diferente de todas las demás expresiones, donde todos los tipos implementados se conocen por adelantado, y el compilador solo tiene que determinar si uno de ellos cumple con los requisitos de restricción.
user31601