Java 8: dov'è TriFunction (e parente) in java.util.function? O qual è l'alternativa?


113

Vedo java.util.function.BiFunction, quindi posso farlo:

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

E se non fosse abbastanza buono e avessi bisogno di TriFunction? Non esiste!

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

Immagino di dover aggiungere che so di poter definire la mia TriFunction, sto solo cercando di capire la logica alla base di non includerla nella libreria standard.


1
con l'interfaccia bifunzione, puoi definire facilmente la classe N-funzione, se definisci trifunzione come interfaccia separata, il primo sb chiederà perché non quadofunzione, e secondo, devi duplicare tutti i metodi che accettano Bifunction come parametro
user902383

6
C'è un punto in cui i rendimenti diminuiscono per API come questa. (Personalmente, penso che JDK8 sia passato un po 'di tempo fa, ma questo va oltre.)
Louis Wasserman

Credo che la logica fosse affermare che Function e BiFunction sono state completamente implementate con oggetti e tipi nativi. Includere TriFunctions con tutte le varie varianti farebbe saltare in aria il JRE con classi e metodi.
Thorbjørn Ravn Andersen

1
Risposta breve. In Java, se non lo vedi, ne crei uno tuo (vedi le risposte di Alex P ovviamente). Nota a margine, in C #, gli implementatori di dotnet ti hanno fornito quelli predefiniti (fino a 16 argomenti), ma senza i nomi del prefisso ("Bi" qui): vedi docs.microsoft.com/en-us/dotnet/api/… Solo un semplice "Func". Quindi questo è uno dei posti che preferisco dotnet su java. Per favore, non trasformare questa sezione di commenti in una guerra mortale. e limitare i commenti solo a BiFunction.
granadaCoder

Risposte:


81

Per quanto ne so, ci sono solo due tipi di funzioni, distruttive e costruttive.

Mentre la funzione costruttiva, come suggerisce il nome, costruisce qualcosa, una distruttiva distrugge qualcosa, ma non nel modo in cui potresti pensare ora.

Ad esempio, la funzione

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

è costruttivo . Poiché hai bisogno di costruire qualcosa. Nell'esempio hai costruito la tupla (x, y) . Le funzioni costruttive hanno il problema di non essere in grado di gestire infiniti argomenti. Ma la cosa peggiore è che non puoi lasciare una discussione aperta. Non puoi semplicemente dire "beh, lascia x: = 1" e provare ogni y che potresti voler provare. Devi costruire ogni volta l'intera tupla con x := 1. Quindi, se ti piace vedere cosa restituiscono le funzioni, y := 1, y := 2, y := 3devi scrivere f(1,1) , f(1,2) , f(1,3).

In Java 8, le funzioni costruttive dovrebbero essere gestite (la maggior parte delle volte) utilizzando riferimenti a metodi perché non c'è molto vantaggio nell'usare una funzione lambda costruttiva. Sono un po 'come i metodi statici. Puoi usarli, ma non hanno uno stato reale.

L'altro tipo è quello distruttivo, prende qualcosa e lo smantella per quanto necessario. Ad esempio, la funzione distruttiva

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

fa lo stesso della funzione fche era costruttiva. I vantaggi di una funzione distruttiva sono che ora puoi gestire infiniti argomenti, il che è particolarmente conveniente per i flussi, e puoi semplicemente lasciare gli argomenti aperti. Quindi, se vuoi di nuovo vedere come sarebbe il risultato se x := 1e y := 1 , y := 2 , y := 3, puoi dire h = g(1)ed h(1)è il risultato per y := 1, h(2)per y := 2e h(3)per y := 3.

Quindi qui hai uno stato fisso! È abbastanza dinamico e la maggior parte delle volte è quello che vogliamo da un lambda.

I pattern come Factory sono molto più facili se puoi semplicemente inserire una funzione che fa il lavoro per te.

Quelli distruttivi si combinano facilmente tra loro. Se il tipo è giusto puoi semplicemente comporli come preferisci. Usandolo, puoi facilmente definire morfismi che rendono (con valori immutabili) i test molto più facili!

Puoi farlo anche con uno costruttivo, ma la composizione distruttiva sembra più carina e più simile a un elenco o un decoratore, e quella costruttiva assomiglia molto a un albero. E cose come il backtracking con funzioni costruttive non sono piacevoli. Puoi semplicemente salvare le funzioni parziali di una distruttiva (programmazione dinamica), e su "backtrack" basta usare la vecchia funzione distruttiva. Ciò rende il codice molto più piccolo e meglio leggibile. Con le funzioni costruttive devi più o meno ricordare tutti gli argomenti, il che può essere molto.

Allora perché c'è bisogno di BiFunctiondovrebbe essere più interrogativo che perché non ce n'è TriFunction?

Prima di tutto, molto tempo hai solo pochi valori (meno di 3) e hai bisogno solo di un risultato, quindi una normale funzione distruttiva non sarebbe affatto necessaria, una costruttiva andrebbe bene. E ci sono cose come le monadi che hanno davvero bisogno di una funzione costruttiva. Ma a parte questo, non ci sono davvero molte buone ragioni per cui esiste BiFunction. Il che non significa che debba essere rimosso! Combatto per le mie Monadi finché non muoio!

Quindi, se hai molti argomenti, che non puoi combinare in una classe contenitore logica, e se hai bisogno che la funzione sia costruttiva, usa un riferimento al metodo. Altrimenti prova a usare la nuova abilità acquisita delle funzioni distruttive, potresti trovarti a fare molte cose con molte meno righe di codice.


2
Hai risposto alla mia domanda ... penso ... non so se i progettisti del linguaggio Java provengono da questa linea di pensiero, ma non sono esperto nella programmazione funzionale. Grazie per la spiegazione.
Richard Finegan

79
Non ho mai visto i termini costruttivi e distruttivi usati per fare riferimento ai concetti che descrivi. Penso che curry e non curry siano termini più comuni.
Feuermurmel

17
Il primo esempio di funzione non è sintatticamente corretto. Dovrebbe essere BiFunction e non Function, perché richiede due argomenti di input.
Annouk

3
IMO è BiFunctionstato creato per consentire una facile riduzione dei dati e la maggior parte delle Streamoperazioni del terminale sono solo riduzioni dei dati. Un buon esempio è BinaryOperator<T>, usato in molti Collectors. Un primo elemento si riduce con il secondo, che poi può essere ridotto con il successivo e così via. Ovviamente, potresti creare un Function<T, Function<T, T>func = x -> (y -> / * codice di riduzione qui * /). Ma sul serio? Tutto questo quando puoi semplicemente farlo BinaryOperator<T> func = (x, y) -> /*reduction code here*/. Inoltre, questo approccio di riduzione dei dati mi sembra molto simile al tuo approccio "distruttivo".
FBB

32
Come ha fatto a ottenere così tanti voti positivi? È una risposta terribile e confusa, perché si basa sulla premessa che Function<Integer,Integer> f = (x,y) -> x + yè Java valido, ma non lo è. Dovrebbe essere una BiFunction per cominciare!
wvdz

162

Se hai bisogno di TriFunction, fallo semplicemente:

@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));
    }
}

Il seguente piccolo programma mostra come può essere utilizzato. Ricorda che il tipo di risultato è specificato come ultimo parametro di tipo generico.

  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]
    }
  }

Immagino che se ci fosse un uso pratico per TriFunction in java.util.*o java.lang.*sarebbe stato definito. Non andrei mai oltre i 22 argomenti, però ;-) Quello che voglio dire con questo, tutto il nuovo codice che consente lo streaming di raccolte non ha mai richiesto TriFunction come nessuno dei parametri del metodo. Quindi non è stato incluso.

AGGIORNARE

Per completezza e seguendo la spiegazione delle funzioni distruttive in un'altra risposta (relativa al currying), ecco come TriFunction può essere emulato senza interfaccia aggiuntiva:

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

Ovviamente è possibile combinare funzioni in altri modi, ad esempio:

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

Mentre il curry sarebbe naturale per qualsiasi linguaggio che supporti la programmazione funzionale oltre i lambda, Java non è costruito in questo modo e, sebbene realizzabile, il codice è difficile da mantenere e talvolta da leggere. Tuttavia, è molto utile come esercizio e talvolta le funzioni parziali hanno un posto legittimo nel codice.


6
Grazie per la soluzione. E sì, c'è sicuramente un uso per BiFunction, TriFunction, ... Altrimenti la gente non lo cercherebbe. Immagino che l'intera cosa lambda sia troppo nuova per Oracle in questo momento e verrà estesa nelle versioni successive di Java. Al momento è più una prova di concetto.
Stefan Endrullis

Hy @Alex puoi definire la seguente riga. cosa succede qui 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)); }
Muneeb Nasir

@MuneebNasir - ti permette di fare la composizione delle funzioni: 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); //12vedi stackoverflow.com/questions/19834611/…
Alex Pakka

Aggiunto andThen()esempio di utilizzo alla risposta.
Alex Pakka

Non solo il curry non è ben adattato al linguaggio Java, ma correggimi anche se sbaglio, ma BiFunctionviene utilizzato Streamnell'API per eseguire la riduzione dei dati, che assomiglia molto all'approccio del currying: non ne prendi mai più di due argomenti e puoi elaborare un numero qualsiasi di elementi, una riduzione alla volta (vedi il mio commento sulla risposta accettata, sarei felice di sapere se sbaglio a vederla in questo modo).
FBB

13

L'alternativa è aggiungere la seguente dipendenza,

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

Ora puoi usare la funzione Vavr, come sotto fino a 8 argomenti,

3 argomenti:

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

5 argomenti:

Function5<Integer, Integer, Integer, Integer, Integer, Integer> f = 
      (a, b, c, d, e) -> a + b + c + d + e;

2
Stavo per aggiornare la mia risposta per menzionare vavr, ma tu eri il primo, quindi ho votato. Se arrivi al punto in cui hai bisogno di una TriFunction, c'è una grande possibilità che tu stia meglio usando la vavrlibreria: rende la programmazione in stile funzionale il più sopportabile possibile in Java.
Alex Pakka

7

Ho quasi la stessa domanda e una risposta parziale. Non sono sicuro che la risposta costruttiva / decostruttiva sia ciò che i progettisti del linguaggio avevano in mente. Penso che avere 3 e più fino a N abbia casi d'uso validi.

Vengo da .NET. e in .NET hai Func e Action per le funzioni void. Esistono anche predicato e alcuni altri casi speciali. Vedi: https://msdn.microsoft.com/en-us/library/bb534960(v=vs.110).aspx

Mi chiedo quale sia stato il motivo per cui i progettisti del linguaggio hanno optato per Function, Bifunction e non hanno continuato fino a DecaExiFunction?

La risposta alla seconda parte è la cancellazione del tipo. Dopo la compilazione non c'è differenza tra Func e Func. Il seguente quindi non 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);
    }
}

Le funzioni interne sono state utilizzate per aggirare un altro problema minore. Eclipse ha insistito per avere entrambe le classi in file denominati Function nella stessa directory ... Non sono sicuro che questo sia un problema del compilatore al giorno d'oggi. Ma non posso disattivare l'errore in Eclipse.

Func è stato utilizzato per prevenire conflitti di nome con il tipo di funzione java.

Quindi, se vuoi aggiungere Func da 3 fino a 16 argomenti, puoi fare due cose.

  • Crea TriFunc, TesseraFunc, PendeFunc, ... DecaExiFunc ecc
    • (Dovrei usare il greco o il latino?)
  • Usa i nomi dei pacchetti o le classi per rendere i nomi diversi.

Esempio per il secondo modo:

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

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

e

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);
    }

Quale sarebbe l'approccio migliore?

Negli esempi precedenti non ho incluso le implementazioni per i metodi andThen () e compose (). Se li aggiungi, devi aggiungere 16 overload ciascuno: il TriFunc dovrebbe avere un andthen () con 16 argomenti. Ciò ti darebbe un errore di compilazione a causa delle dipendenze circolari. Inoltre non avresti questi sovraccarichi per Function e BiFunction. Pertanto dovresti anche definire Func con un argomento e Func con due argomenti. In .NET le dipendenze circolari verrebbero aggirate utilizzando metodi di estensione non presenti in Java.


2
Perché avresti bisogno andThendi 16 argomenti? Il risultato di una funzione in Java è un singolo valore. andThenprende questo valore e fa qualcosa con esso. Inoltre, non ci sono problemi con la denominazione. I nomi delle classi dovrebbero essere diversi e trovarsi in file diversi con lo stesso nome, seguendo la logica impostata dagli sviluppatori del linguaggio Java con Function e BiFunction. Inoltre, tutti questi nomi diversi sono necessari se i tipi di argomenti sono diversi. Si può creare un VargFunction(T, R) { R apply(T.. t) ... }singolo tipo.
Alex Pakka

2

Ho trovato il codice sorgente per BiFunction qui:

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

L'ho modificato per creare TriFunction. Come BiFunction, utilizza andThen () e non compose (), quindi per alcune applicazioni che richiedono compose (), potrebbe non essere appropriato. Dovrebbe andare bene per i normali tipi di oggetti. Un buon articolo su andThen () e compose () può essere trovato qui:

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));
    }
}

2

Puoi anche creare la tua funzione prendendo i 3 parametri

@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

0

Non puoi sempre fermarti a TriFunction. A volte, potrebbe essere necessario passare n numero di parametri alle funzioni. Quindi il team di supporto dovrà creare una QuadFunction per correggere il codice. La soluzione a lungo termine sarebbe creare un oggetto con i parametri extra e quindi utilizzare la funzione o la bifunzione già pronte.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.