java java-stream covariance contravariance invariance

java - ¿Cuáles son las buenas razones para elegir la invariancia en una API como Stream.reduce()?



java-stream covariance (2)

En mi opinión, es solo que no hay un caso de uso real para la mejora propuesta. El Javadoc propuesto tiene 3 parámetros de tipo más y 5 comodines más. Supongo que es suficiente para simplificar todo a la API oficial porque los desarrolladores regulares de Java no quieren (a menudo ni siquiera son capaces) perder la cabeza tratando de hacer feliz al compilador. Solo para el registro, su reduce() tiene 165 caracteres en la firma de tipo solamente.

Además, los argumentos para .reduce() menudo se suministran en forma de expresiones lambda, por lo que no tiene sentido tener versiones más versátiles cuando tales expresiones a menudo no tienen lógica comercial o son muy simples y, por lo tanto, se usan solo una vez.

Por ejemplo, soy usuario de su fantástica biblioteca jOOQ y también soy un curioso desarrollador de Java que ama los rompecabezas genéricos, pero a menudo me olvido de la simplicidad de las tuplas de SQL cuando tengo que poner comodines en mis propias interfaces debido al parámetro de tipo en Result<T> y el tipo de problemas que genera cuando se trata de interfaces de los tipos de registro, no es que sea una falla jOOQ

Al revisar el diseño de la API de Java 8 Stream , me sorprendió la invariancia genérica de los argumentos de Stream.reduce() :

<U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)

Una versión aparentemente más versátil de la misma API podría haber aplicado covarianza / contravarianza en referencias individuales a U , como:

<U> U reduce(U identity, BiFunction<? super U, ? super T, ? extends U> accumulator, BiFunction<? super U, ? super U, ? extends U> combiner)

Esto permitiría lo siguiente, que no es posible actualmente:

// Assuming we want to reuse these tools all over the place: BiFunction<Number, Number, Double> numberAdder = (t, u) -> t.doubleValue() + u.doubleValue(); // This currently doesn''t work, but would work with the suggestion Stream<Number> stream = Stream.of(1, 2L, 3.0); double sum = stream.reduce(0.0, numberAdder, numberAdder);

Para solucionar el problema, use las referencias de los métodos para "forzar" los tipos en el tipo de destino:

double sum = stream.reduce(0.0, numberAdder::apply, numberAdder::apply);

C # no tiene este problema en particular, ya que Func(T1, T2, TResult) se define de la siguiente manera, utilizando la varianza del sitio de declaración, lo que significa que cualquier API que use Func obtiene este comportamiento de forma gratuita:

public delegate TResult Func<in T1, in T2, out TResult>( T1 arg1, T2 arg2 )

¿Cuáles son las ventajas (y posiblemente, las razones de las decisiones de EG) del diseño existente sobre el diseño sugerido?

O, preguntado de manera diferente, ¿cuáles son las advertencias del diseño sugerido que podría pasar por alto (p. Ej., Dificultades de inferencia de tipo, restricciones de paralelización o restricciones específicas de la operación de reducción como, por ejemplo, asociatividad, anticipación de una futura BiFunction<in T, in U, out R> de declaración de Java en BiFunction<in T, in U, out R> , ...)?


Rastrear la historia del desarrollo lambda y aislar la razón "THE" de esta decisión es difícil, por lo que eventualmente, uno tendrá que esperar a que uno de los desarrolladores responda esta pregunta.

Algunos consejos pueden ser los siguientes:

  • Las interfaces de flujo han sufrido varias iteraciones y refactorizaciones. En una de las primeras versiones de la interfaz de Stream , se han dedicado métodos de reduce , y el que está más cerca del método de reduce en la pregunta todavía se llamaba Stream#fold ese entonces. Este ya recibió un BinaryOperator como el parámetro combiner .

  • Curiosamente, durante bastante tiempo, la propuesta lambda incluyó una interfaz dedicada Combiner<T,U,R> . Contraintuitivamente, esto no se usó como combiner en la función de Stream#reduce la Stream#reduce . En cambio, se usó como reducer , que parece ser lo que hoy en día se conoce como el accumulator . Sin embargo, la interfaz de Combiner se reemplazó con BiFunction en una revisión posterior .

  • La similitud más sorprendente con la pregunta aquí se encuentra en un hilo sobre la firma Stream#flatMap en la lista de correo , que luego se convierte en la pregunta general sobre las variaciones de las firmas del método de transmisión. Los arreglaron en algunos lugares, por ejemplo.

    Como Brian me corrige:

    <R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);

    en lugar de:

    <R> Stream<R> flatMap(Function<T, Stream<? extends R>> mapper);

    Pero notó que en algunos lugares, esto no era posible:

    T reduce(T identity, BinaryOperator<T> accumulator);

    y

    Optional<T> reduce(BinaryOperator<T> accumulator);

    No se puede arreglar porque usaron ''BinaryOperator'', pero si se usa ''BiFunction'' entonces tenemos más flexibilidad

    <U> U reduce(U identity, BiFunction<? super U, ? super T, ? extends U> accumulator, BinaryOperator<U> combiner)

    En lugar de:

    <U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);

    Mismo comentario con respecto a ''BinaryOperator''

    (énfasis por mi).

La única justificación que encontré para no reemplazar el BinaryOperator con un BiFunction fue finalmente dada en la respuesta a esta declaración, en el mismo hilo :

BinaryOperator no será reemplazado por BiFunction incluso si, como usted dijo, introduce más flexibilidad, un BinaryOperator solicita que los dos parámetros y el tipo de retorno sean iguales, por lo que conceptualmente tiene más peso (el GE ya vota al respecto).

Tal vez alguien pueda desenterrar una referencia específica del voto del Grupo de Expertos que gobernó esta decisión, pero tal vez esta cita ya responde suficientemente a la pregunta de por qué es así ...