Java 8: ¿Dónde está TriFunction (y kin) en java.util.function? ¿O cuál es la alternativa?

113

Veo java.util.function.BiFunction, entonces puedo hacer esto:

BiFunction<Integer, Integer, Integer> f = (x, y) -> { return 0; };

¿Qué pasa si eso no es lo suficientemente bueno y necesito TriFunction? ¡No existe!

TriFunction<Integer, Integer, Integer, Integer> f = (x, y, z) -> { return 0; };

Supongo que debo agregar que sé que puedo definir mi propia TriFunction, solo estoy tratando de entender la razón detrás de no incluirla en la biblioteca estándar.

Richard Finegan
fuente
1
con la interfaz bifunción, puede definir fácilmente la clase de función N, si define la trifunción como una interfaz separada, el primer sb preguntará por qué no la función cuádruple, y en segundo lugar, debe duplicar todos los métodos que toman Bifunction como parámetro
user902383
6
Existe un punto de rendimiento decreciente para API como esta. (Personalmente, creo que JDK8 lo aprobó hace un tiempo, pero esto va más allá incluso de eso).
Louis Wasserman
Creo que el fundamento fue decir que Function y BiFunction se implementaron completamente con objetos y tipos nativos. Incluir TriFunctions con todas las diversas variaciones haría explotar el JRE con clases y métodos.
Thorbjørn Ravn Andersen
1
Respuesta corta. En Java, si no lo ve, cree el suyo propio (consulte las respuestas de Alex P, por supuesto). Nota al margen, en C #, los implementadores de dotnet le proporcionaron los predefinidos (hasta 16 argumentos), pero sin los nombres de prefijo ("Bi" aquí): consulte docs.microsoft.com/en-us/dotnet/api/… Solo un simple "Func". Así que este es uno de los lugares en los que prefiero dotnet sobre java. Por favor, no convierta esta sección de comentarios en una guerra h0ly. y limitar los comentarios a BiFunction únicamente.
granadaCoder

Respuestas:

81

Hasta donde yo sé, solo hay dos tipos de funciones, destructivas y constructivas.

Mientras que la función constructiva, como su nombre lo indica, construye algo, una destructiva destruye algo, pero no de la forma en que piensas ahora.

Por ejemplo, la función

Function<Integer,Integer> f = (x,y) -> x + y  

es constructivo . Como necesitas construir algo. En el ejemplo, construiste la tupla (x, y) . Las funciones constructivas tienen el problema de no poder manejar argumentos infinitos. Pero lo peor es que no puedes dejar una discusión abierta. No puedes simplemente decir "bueno, deja x: = 1" y probar cada y que quieras probar. Tienes que construir cada vez toda la tupla con x := 1. Entonces, si desea ver qué devuelven las funciones y := 1, y := 2, y := 3, debe escribir f(1,1) , f(1,2) , f(1,3).

En Java 8, las funciones constructivas deben manejarse (la mayoría de las veces) mediante el uso de referencias de métodos porque no hay mucha ventaja de usar una función lambda constructiva. Son un poco como métodos estáticos. Puede usarlos, pero no tienen un estado real.

El otro tipo es el destructivo, toma algo y lo desmantela tanto como sea necesario. Por ejemplo, la función destructiva

Function<Integer, Function<Integer, Integer>> g = x -> (y -> x + y) 

hace lo mismo que la función fque fue constructiva. Los beneficios de una función destructiva son que ahora puede manejar argumentos infinitos, lo que es especialmente conveniente para flujos, y puede dejar los argumentos abiertos. Entonces, si desea ver nuevamente cómo sería el resultado si x := 1y y := 1 , y := 2 , y := 3, puede decir h = g(1)y h(1)es el resultado para y := 1, h(2)para y := 2y h(3)para y := 3.

¡Así que aquí tienes un estado fijo! Eso es bastante dinámico y eso es la mayoría de las veces lo que queremos de una lambda.

Patrones como Factory son mucho más fáciles si puede poner una función que haga el trabajo por usted.

Los destructivos se combinan fácilmente entre sí. Si el tipo es correcto, puede componerlos como desee. ¡Con eso, puede definir fácilmente morfismos que hacen que las pruebas (con valores inmutables) sean mucho más fáciles!

También puedes hacer eso con una constructiva, pero la composición destructiva se ve mejor y más como una lista o un decorador, y la constructiva se parece mucho a un árbol. Y cosas como retroceder con funciones constructivas simplemente no son agradables. Puede simplemente guardar las funciones parciales de una destructiva (programación dinámica), y en "retroceder" simplemente use la antigua función destructiva. Eso hace que el código sea mucho más pequeño y mejor legible. Con funciones constructivas tienes más o menos que recordar todos los argumentos, que pueden ser muchos.

Entonces, ¿por qué es necesario BiFunctionque haya más preguntas que por qué no TriFunction?

En primer lugar, muchas veces solo tiene unos pocos valores (menos de 3) y solo necesita un resultado, por lo que una función destructiva normal no sería necesaria en absoluto, una constructiva estaría bien. Y hay cosas como las mónadas que realmente necesitan una función constructiva. Pero aparte de eso, en realidad no hay muchas buenas razones por las que existe un BiFunction. ¡Lo que no significa que deba eliminarse! ¡Lucho por mis Mónadas hasta que muera!

Entonces, si tiene muchos argumentos, que no puede combinar en una clase de contenedor lógico, y si necesita que la función sea constructiva, use una referencia de método. De lo contrario, intente utilizar la nueva capacidad adquirida de funciones destructivas, puede encontrarse haciendo muchas cosas con muchas menos líneas de código.

usuario3003859
fuente
2
Respondiste a mi pregunta ... Creo ... No sé si los diseñadores del lenguaje Java vienen de esta línea de pensamiento, pero no estoy bien versado en programación funcional. Gracias por la explicación.
Richard Finegan
79
Nunca he visto que se usen los términos constructivo y destructivo para referirse a los conceptos que describe. Creo que curry y no curry son términos más comunes.
Feuermurmel
17
El primer ejemplo de función no es sintácticamente correcto. Debe ser BiFunction y no Function, porque toma dos argumentos de entrada.
annouk
3
IMO BiFunctionfue creado para permitir una fácil reducción de datos, y la mayoría de Streamlas operaciones de la terminal son solo reducciones de datos. Un buen ejemplo es el que se BinaryOperator<T>utiliza en muchos Collectors. Un primer elemento se reduce con el segundo, que luego se puede reducir con el siguiente, y así sucesivamente. Por supuesto, puede crear un Function<T, Function<T, T>func = x -> (y -> / * código de reducción aquí * /). ¿Pero en serio? Todo esto cuando simplemente puedes hacerlo BinaryOperator<T> func = (x, y) -> /*reduction code here*/. Además, este enfoque de reducción de datos se parece mucho a su enfoque "destructivo" para mí.
FBB
32
¿Cómo consiguió esto tantos votos a favor? Es una respuesta terrible y confusa, porque se basa en la premisa de que Function<Integer,Integer> f = (x,y) -> x + yes Java válido, que no lo es. ¡Para empezar, debería ser una BiFunción!
wvdz
162

Si necesita TriFunction, simplemente haga esto:

@FunctionalInterface
interface TriFunction<A,B,C,R> {

    R apply(A a, B b, C c);

    default <V> TriFunction<A, B, C, V> andThen(
                                Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (A a, B b, C c) -> after.apply(apply(a, b, c));
    }
}

El siguiente pequeño programa muestra cómo se puede utilizar. Recuerde que el tipo de resultado se especifica como último parámetro de tipo genérico.

  public class Main {

    public static void main(String[] args) {
        BiFunction<Integer, Long, String> bi = (x,y) -> ""+x+","+y;
        TriFunction<Boolean, Integer, Long, String> tri = (x,y,z) -> ""+x+","+y+","+z;


        System.out.println(bi.apply(1, 2L)); //1,2
        System.out.println(tri.apply(false, 1, 2L)); //false,1,2

        tri = tri.andThen(s -> "["+s+"]");
        System.out.println(tri.apply(true,2,3L)); //[true,2,3]
    }
  }

Supongo que si hubiera un uso práctico para TriFunction en java.util.*o java.lang.*se habría definido. Sin embargo, nunca iría más allá de los 22 argumentos ;-) Lo que quiero decir con eso, todo el código nuevo que permite transmitir colecciones nunca requirió TriFunction como ninguno de los parámetros del método. Entonces no fue incluido.

ACTUALIZAR

Para completar y seguir la explicación de las funciones destructivas en otra respuesta (relacionada con el curado), así es como se puede emular TriFunction sin una interfaz adicional:

Function<Integer, Function<Integer, UnaryOperator<Integer>>> tri1 = a -> b -> c -> a + b + c;
System.out.println(tri1.apply(1).apply(2).apply(3)); //prints 6

Por supuesto, es posible combinar funciones de otras formas, por ejemplo:

BiFunction<Integer, Integer, UnaryOperator<Integer>> tri2 = (a, b) -> c -> a + b + c;
System.out.println(tri2.apply(1, 2).apply(3)); //prints 6
//partial function can be, of course, extracted this way
UnaryOperator partial = tri2.apply(1,2); //this is partial, eq to c -> 1 + 2 + c;
System.out.println(partial.apply(4)); //prints 7
System.out.println(partial.apply(5)); //prints 8

Si bien el curry sería natural para cualquier lenguaje que admita programación funcional más allá de lambdas, Java no está construido de esta manera y, aunque se puede lograr, el código es difícil de mantener y, a veces, de leer. Sin embargo, es muy útil como ejercicio y, a veces, las funciones parciales tienen un lugar legítimo en su código.

Alex Pakka
fuente
6
Gracias por la solucion Y sí, definitivamente hay uso para BiFunction, TriFunction, ... De lo contrario, la gente no lo buscaría. Supongo que todo el asunto de lambda es demasiado nuevo para Oracle en este momento y se extenderá en versiones posteriores de Java. Por el momento es más una prueba de concepto.
Stefan Endrullis
Hy @Alex, ¿puede definir la siguiente línea? ¿Qué está pasando aquí por defecto <V> TriFunction <A, B, C, V> y luego (Función <? super R,? extiende V> después) {Objects.requireNonNull (después); return (A a, B b, C c) -> after.apply (aplicar (a, b, c)); }
Muneeb Nasir
@MuneebNasir: le permite realizar la composición de funciones: TriFunction<Integer,Integer,Integer,Integer> comp = (x,y,z) -> x + y + z; comp = comp.andThen(s -> s * 2); int result = comp.apply(1, 2, 3); //12consulte stackoverflow.com/questions/19834611/…
Alex Pakka
Se agregó un andThen()ejemplo de uso a la respuesta.
Alex Pakka
No solo el currying no está bien adaptado al lenguaje Java, sino que también, corrígeme si me equivoco, pero BiFunctionse usa en la StreamAPI para realizar la reducción de datos, que se parece mucho al enfoque de currying para mí: nunca tomas más de dos argumentos, y puede procesar cualquier cantidad de elementos, una reducción a la vez (vea mi comentario sobre la respuesta aceptada, me complacería saber si me equivoco al verlo de esa manera).
FBB
13

La alternativa es agregar la siguiente dependencia,

<dependency>
    <groupId>io.vavr</groupId>
    <artifactId>vavr</artifactId>
    <version>0.9.0</version>
</dependency>

Ahora, puede usar la función Vavr, como a continuación hasta 8 argumentos,

3 argumentos:

Function3<Integer, Integer, Integer, Integer> f = 
      (a, b, c) -> a + b + c;

5 argumentos:

Function5<Integer, Integer, Integer, Integer, Integer, Integer> f = 
      (a, b, c, d, e) -> a + b + c + d + e;
Amol Damodar
fuente
2
Estaba a punto de actualizar mi respuesta para mencionar vavr, pero tú eras el primero, así que voté a favor. Si llega al punto en que necesita una TriFunction, existe una gran posibilidad de que esté mejor utilizando la vavrbiblioteca: hace que la programación de estilo funcional sea lo más soportable posible en Java.
Alex Pakka
7

Tengo casi la misma pregunta y una respuesta parcial. No estoy seguro de si la respuesta constructiva / deconstructiva es la que tenían en mente los diseñadores del lenguaje. Creo que tener 3 y más hasta N tiene casos de uso válidos.

Vengo de .NET. y en .NET tienes Func y Action para funciones void. También existen predicados y algunos otros casos especiales. Ver: https://msdn.microsoft.com/en-us/library/bb534960(v=vs.110).aspx

Me pregunto cuál fue la razón por la que los diseñadores de lenguajes optaron por Function, Bifunction y no continuaron hasta DecaExiFunction.

La respuesta a la segunda parte es el borrado de tipo. Después de la compilación, no hay diferencia entre Func y Func. Por lo tanto, lo siguiente no se compila:

package eu.hanskruse.trackhacks.joepie;

public class Functions{

    @FunctionalInterface
    public interface Func<T1,T2,T3,R>{
        public R apply(T1 t1,T2 t2,T3 t3);
    }

    @FunctionalInterface
    public interface Func<T1,T2,T3,T4,R>{
        public R apply(T1 t1,T2 t2,T3 t3, T4 t4);
    }
}

Las funciones internas se utilizaron para sortear otro problema menor. Eclipse insistió en tener ambas clases en archivos llamados Función en el mismo directorio ... No estoy seguro de si esto es un problema del compilador hoy en día. Pero no puedo convertir el error en Eclipse.

Func se utilizó para evitar conflictos de nombres con el tipo de función java.

Entonces, si desea agregar Func desde 3 hasta 16 argumentos, puede hacer dos cosas.

  • Haga TriFunc, TesseraFunc, PendeFunc, ... DecaExiFunc, etc.
    • (¿Debería usar griego o latín?)
  • Utilice nombres de paquetes o clases para hacer que los nombres sean diferentes.

Ejemplo de la segunda forma:

 package eu.hanskruse.trackhacks.joepie.functions.tri;

        @FunctionalInterface
        public interface Func<T1,T2,T3,R>{
            public R apply(T1 t1,T2 t2,T3 t3);
        }

y

package eu.trackhacks.joepie.functions.tessera;

    @FunctionalInterface
    public interface Func<T1,T2,T3,T4,R>{
        public R apply(T1 t1,T2 t2,T3 t3, T4 t4);
    }

¿Cuál sería el mejor enfoque?

En los ejemplos anteriores no incluí implementaciones para los métodos andThen () y compose (). Si agrega estos, debe agregar 16 sobrecargas cada uno: el TriFunc debe tener un and then () con 16 argumentos. Eso le daría un error de compilación debido a dependencias circulares. Además, no tendría estas sobrecargas para Function y BiFunction. Por lo tanto, también debe definir Func con un argumento y Func con dos argumentos. En .NET, las dependencias circulares se eludirían utilizando métodos de extensión que no están presentes en Java.

Hans
fuente
2
¿Por qué necesitarías andThencon 16 argumentos? El resultado de una función en Java es un valor único. andThentoma este valor y hace algo con él. Además, no hay ningún problema con los nombres. Los nombres de las clases deben ser diferentes y estar en diferentes archivos con el mismo nombre, siguiendo la lógica establecida por los desarrolladores del lenguaje Java con Function y BiFunction. Además, todos estos nombres diferentes son necesarios si los tipos de argumentos son diferentes. Se puede crear un VargFunction(T, R) { R apply(T.. t) ... }solo tipo.
Alex Pakka
2

Encontré el código fuente de BiFunction aquí:

https://github.com/JetBrains/jdk8u_jdk/blob/master/src/share/classes/java/util/function/BiFunction.java

Lo modifiqué para crear TriFunction. Como BiFunction, utiliza andThen () y not compose (), por lo que para algunas aplicaciones que requieren compose (), puede que no sea apropiado. Debería estar bien para tipos de objetos normales. Un buen artículo sobre andThen () y compose () se puede encontrar aquí:

http://www.deadcoderising.com/2015-09-07-java-8-functional-composition-using-compose-and-andthen/

import java.util.Objects;
import java.util.function.Function;

/**
 * Represents a function that accepts two arguments and produces a result.
 * This is the three-arity specialization of {@link Function}.
 *
 * <p>This is a <a href="package-summary.html">functional interface</a>
 * whose functional method is {@link #apply(Object, Object)}.
 *
 * @param <S> the type of the first argument to the function
 * @param <T> the type of the second argument to the function
 * @param <U> the type of the third argument to the function
 * @param <R> the type of the result of the function
 *
 * @see Function
 * @since 1.8
 */
@FunctionalInterface
public interface TriFunction<S, T, U, R> {

    /**
     * Applies this function to the given arguments.
     *
     * @param s the first function argument
     * @param t the second function argument
     * @param u the third function argument
     * @return the function result
     */
    R apply(S s, T t, U u);

    /**
     * Returns a composed function that first applies this function to
     * its input, and then applies the {@code after} function to the result.
     * If evaluation of either function throws an exception, it is relayed to
     * the caller of the composed function.
     *
     * @param <V> the type of output of the {@code after} function, and of the
     *           composed function
     * @param after the function to apply after this function is applied
     * @return a composed function that first applies this function and then
     * applies the {@code after} function
     * @throws NullPointerException if after is null
     */
    default <V> TriFunction<S, T, U, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (S s, T t, U u) -> after.apply(apply(s, t, u));
    }
}
Vance
fuente
2

También puede crear su propia función tomando los 3 parámetros

@FunctionalInterface
public interface MiddleInterface<F,T,V>{
    boolean isBetween(F from, T to, V middleValue);
}

MiddleInterface<Integer, Integer, Integer> middleInterface = 
(x,y,z) -> x>=y && y<=z; // true
Leandro Maro
fuente
0

No siempre puede detenerse en TriFunction. A veces, es posible que deba pasar un número n de parámetros a sus funciones. Luego, el equipo de soporte tendrá que crear una QuadFunction para corregir su código. La solución a largo plazo sería crear un Objeto con los parámetros adicionales y luego usar la Función o BiFunción lista para usar.

Koushik Roy
fuente