Java 8: Wo ist TriFunction (und Kin) in java.util.function? Oder was ist die Alternative?

113

Ich sehe java.util.function.BiFunction, also kann ich das tun:

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

Was ist, wenn das nicht gut genug ist und ich TriFunction brauche? Es existiert nicht!

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

Ich denke, ich sollte hinzufügen, dass ich weiß, dass ich meine eigene TriFunction definieren kann. Ich versuche nur zu verstehen, warum sie nicht in die Standardbibliothek aufgenommen wurde.

Richard Finegan
quelle
1
Mit der Bifunktionsschnittstelle können Sie die N-Funktionsklasse einfach definieren. Wenn Sie die Trifunktion als separate Schnittstelle definieren, werden Sie zuerst gefragt, warum nicht die Quadofunktion, und zweitens müssen Sie alle Methoden duplizieren, die Bifunktion als Parameter verwenden
user902383
6
Es gibt einen Punkt, an dem die Renditen für solche APIs sinken. (Ich persönlich denke, JDK8 hat es vor einiger Zeit bestanden, aber das geht sogar darüber hinaus.)
Louis Wasserman
Ich glaube, das Grundprinzip war zu sagen, dass Function und BiFunction vollständig mit Objekten und nativen Typen implementiert wurden. Das Einbeziehen von TriFunctions mit all den verschiedenen Variationen würde die JRE mit Klassen und Methoden in die Luft jagen.
Thorbjørn Ravn Andersen
1
Kurze Antwort. Wenn Sie es in Java nicht sehen, erstellen Sie Ihr eigenes (siehe natürlich Alex P-Antworten). Nebenbei bemerkt, in C # gaben Ihnen die Implementierer von dotnet vordefinierte (bis zu 16 Argumente), jedoch ohne die Präfixnamen ("Bi" hier): siehe docs.microsoft.com/en-us/dotnet/api/… Nur eine einfache "Func". Dies ist einer der Orte, an denen ich Dotnet Java vorziehe. Bitte verwandeln Sie diesen Kommentarbereich nicht in einen Krieg. und beschränken Sie Kommentare nur auf BiFunction.
GranadaCoder

Antworten:

81

Soweit ich weiß, gibt es nur zwei Arten von Funktionen, destruktive und konstruktive.

Während konstruktive Funktionen, wie der Name schon sagt, etwas konstruieren, zerstört eine destruktive Funktion etwas, aber nicht so, wie Sie jetzt vielleicht denken.

Zum Beispiel die Funktion

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

ist konstruktiv . Da musst du etwas konstruieren. Im Beispiel haben Sie das Tupel (x, y) konstruiert . Konstruktive Funktionen haben das Problem, unendliche Argumente nicht verarbeiten zu können. Aber das Schlimmste ist, man kann nicht einfach ein Argument offen lassen. Sie können nicht einfach "gut, lassen Sie x: = 1" sagen und jedes y ausprobieren, das Sie ausprobieren möchten. Sie müssen jedes Mal das ganze Tupel mit konstruieren x := 1. Wenn Sie also sehen möchten, was die Funktionen für y := 1, y := 2, y := 3Sie zurückgeben, müssen Sie schreiben f(1,1) , f(1,2) , f(1,3).

In Java 8 sollten konstruktive Funktionen (meistens) mithilfe von Methodenreferenzen behandelt werden, da die Verwendung einer konstruktiven Lambda-Funktion keinen großen Vorteil bietet. Sie sind ein bisschen wie statische Methoden. Sie können sie verwenden, aber sie haben keinen wirklichen Zustand.

Der andere Typ ist der zerstörerische, er nimmt etwas und zerlegt es so weit wie nötig. Zum Beispiel die destruktive Funktion

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

macht das gleiche wie die Funktion, fdie konstruktiv war. Die Vorteile einer destruktiven Funktion sind, dass Sie jetzt unendlich viele Argumente verarbeiten können, was besonders für Streams praktisch ist, und dass Sie Argumente einfach offen lassen können. Wenn Sie also noch einmal sehen möchten, wie das Ergebnis wäre, wenn x := 1und y := 1 , y := 2 , y := 3, können Sie sagen h = g(1)und h(1)ist das Ergebnis für y := 1, h(2)für y := 2und h(3)für y := 3.

Hier haben Sie also einen festen Zustand! Das ist ziemlich dynamisch und das ist meistens das, was wir von einem Lambda wollen.

Muster wie Factory sind viel einfacher, wenn Sie nur eine Funktion eingeben können, die die Arbeit für Sie erledigt.

Zerstörerische lassen sich leicht miteinander kombinieren. Wenn der Typ stimmt, können Sie sie einfach nach Ihren Wünschen zusammenstellen. Auf diese Weise können Sie leicht Morphismen definieren, die das Testen (mit unveränderlichen Werten) erheblich erleichtern!

Sie können das auch mit einer konstruktiven Komposition tun, aber eine destruktive Komposition sieht besser aus und ähnelt eher einer Liste oder einem Dekorateur, und die konstruktive Komposition ähnelt einem Baum. Und Dinge wie Backtracking mit konstruktiven Funktionen sind einfach nicht schön. Sie können nur die Teilfunktionen einer destruktiven (dynamische Programmierung) speichern und auf "Backtrack" einfach die alte destruktive Funktion verwenden. Das macht Code viel kleiner und besser lesbar. Mit konstruktiven Funktionen müssen Sie sich mehr oder weniger an alle Argumente erinnern, was sehr viel sein kann.

Warum BiFunctionsollte es also mehr Fragen geben als warum gibt es keine TriFunction?

Erstens haben Sie viel Zeit nur ein paar Werte (weniger als 3) und benötigen nur ein Ergebnis, sodass eine normale destruktive Funktion überhaupt nicht benötigt wird, eine konstruktive würde gut funktionieren. Und es gibt Dinge wie Monaden, die wirklich eine konstruktive Funktion brauchen. Aber abgesehen davon gibt es nicht wirklich viele gute Gründe, warum es BiFunctionüberhaupt einen gibt. Was nicht heißt, dass es entfernt werden sollte! Ich kämpfe für meine Monaden, bis ich sterbe!

Wenn Sie also viele Argumente haben, die Sie nicht zu einer logischen Containerklasse kombinieren können, und wenn die Funktion konstruktiv sein soll, verwenden Sie eine Methodenreferenz. Wenn Sie andernfalls versuchen, die neu gewonnene Fähigkeit destruktiver Funktionen zu nutzen, werden Sie möglicherweise viele Dinge mit viel weniger Codezeilen tun.

user3003859
quelle
2
Sie haben meine Frage beantwortet ... Ich denke ... Ich weiß nicht, ob die Java-Sprachdesigner aus dieser Denkrichtung stammen, aber ich bin nicht mit funktionaler Programmierung vertraut. Vielen Dank für die Erklärung.
Richard Finegan
79
Ich habe noch nie gesehen, dass die Begriffe konstruktiv und destruktiv verwendet werden, um sich auf die von Ihnen beschriebenen Konzepte zu beziehen. Ich denke, Curry und Nicht-Curry sind häufigere Begriffe.
Feuermurmel
17
Das erste Funktionsbeispiel ist syntaktisch nicht korrekt. Es sollte BiFunction und nicht Function sein, da zwei Eingabeargumente erforderlich sind.
Annouk
3
IMO BiFunctionwurde entwickelt, um eine einfache Datenreduktion zu ermöglichen, und die meisten StreamTerminaloperationen sind nur Datenreduktionen. Ein gutes Beispiel ist BinaryOperator<T>, in vielen verwendet Collectors. Ein erstes Element wird mit dem zweiten reduziert, das dann mit dem nächsten reduziert werden kann, und so weiter. Natürlich können Sie auch einen Function<T, Function<T, T>func = x -> (y -> / * Reduktionscode hier * /) erstellen. Aber ernsthaft? All dies, wenn Sie es einfach tun können BinaryOperator<T> func = (x, y) -> /*reduction code here*/. Außerdem scheint mir dieser Datenreduktionsansatz Ihrem "destruktiven" Ansatz sehr ähnlich zu sein.
FBB
32
Wie kam es zu so vielen positiven Stimmen? Es ist eine schreckliche und verwirrende Antwort, weil sie auf der Prämisse basiert, dass Function<Integer,Integer> f = (x,y) -> x + yJava gültig ist, was es nicht ist. Das sollte zunächst eine BiFunktion sein!
wvdz
162

Wenn Sie TriFunction benötigen, gehen Sie einfach folgendermaßen vor:

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

Das folgende kleine Programm zeigt, wie es verwendet werden kann. Denken Sie daran, dass der Ergebnistyp als letzter generischer Typparameter angegeben wird.

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

Ich denke, wenn es eine praktische Verwendung für TriFunction in gegeben hätte java.util.*oder java.lang.*es definiert worden wäre. Ich würde jedoch nie über 22 Argumente hinausgehen ;-) Was ich damit meine, alle neuen Codes, die das Streamen von Sammlungen ermöglichen, erforderten niemals TriFunction als einen der Methodenparameter. Es war also nicht enthalten.

AKTUALISIEREN

Der Vollständigkeit halber und gemäß der Erklärung der destruktiven Funktionen in einer anderen Antwort (in Bezug auf Currying) kann TriFunction wie folgt ohne zusätzliche Schnittstelle emuliert werden:

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

Natürlich ist es möglich, Funktionen auf andere Weise zu kombinieren, z.

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

Während Currying für jede Sprache selbstverständlich ist, die funktionale Programmierung über Lambdas hinaus unterstützt, ist Java nicht auf diese Weise aufgebaut, und obwohl dies erreichbar ist, ist der Code schwer zu pflegen und manchmal zu lesen. Als Übung ist es jedoch sehr hilfreich, und manchmal haben Teilfunktionen einen rechtmäßigen Platz in Ihrem Code.

Alex Pakka
quelle
6
Danke für die Lösung. Und ja, es gibt definitiv Verwendung für BiFunction, TriFunction, ... Sonst würden die Leute nicht danach suchen. Vermutlich ist das ganze Lambda-Ding für Oracle momentan einfach zu neu und wird in späteren Java-Versionen erweitert. Im Moment ist es eher ein Proof of Concept.
Stefan Endrullis
Hy @Alex können Sie bitte folgende Zeile definieren. Was passiert hier? Standard <V> TriFunction <A, B, C, V> und dann (Funktion <? super R ,? erweitert V> nach) {Objects.requireNonNull (nach); return (A a, B b, C c) -> after.apply (anwenden (a, b, c)); }
Muneeb Nasir
@ MuneebNasir - es ermöglicht Ihnen, Funktionskomposition zu machen: 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); //12Siehe stackoverflow.com/questions/19834611/…
Alex Pakka
andThen()Anwendungsbeispiel zur Antwort hinzugefügt .
Alex Pakka
Currying ist nicht nur nicht gut an die Java-Sprache angepasst, sondern korrigiert mich auch, wenn ich falsch liege, sondern BiFunctionwird in der StreamAPI verwendet, um eine Datenreduktion durchzuführen, die dem Currying-Ansatz für mich sehr ähnlich ist: Sie nehmen nie mehr als zwei Argumente, und Sie können eine beliebige Anzahl von Elementen nacheinander verarbeiten (siehe meinen Kommentar zur akzeptierten Antwort, ich würde mich freuen zu wissen, wenn ich mich irre, wenn ich das so sehe).
FBB
13

Alternativ können Sie die folgende Abhängigkeit hinzufügen:

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

Jetzt können Sie die Vavr-Funktion verwenden, wie unten bis zu 8 Argumente.

3 Argumente:

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

5 Argumente:

Function5<Integer, Integer, Integer, Integer, Integer, Integer> f = 
      (a, b, c, d, e) -> a + b + c + d + e;
Amol Damodar
quelle
2
Ich wollte gerade meine Antwort aktualisieren, um vavr zu erwähnen, aber Sie waren die Ersten, also habe ich gestimmt. Wenn Sie an den Punkt kommen, an dem Sie eine TriFunction benötigen, besteht eine große Chance, dass Sie mit der vavrBibliothek besser dran sind - dies macht die funktionale Programmierung in Java so erträglich wie möglich.
Alex Pakka
7

Ich habe fast die gleiche Frage und eine teilweise Antwort. Ich bin mir nicht sicher, ob die konstruktive / dekonstruktive Antwort den Sprachdesignern entspricht. Ich denke, 3 und mehr bis zu N zu haben, hat gültige Anwendungsfälle.

Ich komme aus .NET. und in .NET haben Sie Func und Action für ungültige Funktionen. Prädikat und einige andere Sonderfälle existieren ebenfalls. Siehe: https://msdn.microsoft.com/en-us/library/bb534960(v=vs.110).aspx

Ich frage mich, warum die Sprachdesigner sich für Function, Bifunction entschieden haben und erst mit DecaExiFunction fortfuhren.

Die Antwort auf den zweiten Teil lautet Typlöschung. Nach der Kompilierung gibt es keinen Unterschied zwischen Func und Func. Folgendes wird daher nicht kompiliert:

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

Innere Funktionen wurden verwendet, um ein weiteres kleines Problem zu umgehen. Eclipse bestand darauf, beide Klassen in Dateien mit dem Namen Function im selben Verzeichnis zu haben ... Ich bin mir nicht sicher, ob dies heutzutage ein Compiler-Problem ist. Aber ich kann den Fehler in Eclipse nicht beheben.

Func wurde verwendet, um Namenskonflikte mit dem Java-Funktionstyp zu verhindern.

Wenn Sie also Func aus 3 bis 16 Argumenten hinzufügen möchten, können Sie zwei Dinge tun.

  • Machen Sie TriFunc, TesseraFunc, PendeFunc, ... DecaExiFunc etc.
    • (Soll ich Griechisch oder Latein verwenden?)
  • Verwenden Sie Paketnamen oder Klassen, um die Namen zu unterscheiden.

Beispiel für den zweiten Weg:

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

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

und

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

Was wäre der beste Ansatz?

In den obigen Beispielen habe ich keine Implementierungen für die Methoden andThen () und compose () aufgenommen. Wenn Sie diese hinzufügen, müssen Sie jeweils 16 Überladungen hinzufügen: Der TriFunc sollte ein andthen () mit 16 Argumenten haben. Das würde Ihnen einen Kompilierungsfehler aufgrund zirkulärer Abhängigkeiten geben. Außerdem hätten Sie diese Überladungen für Function und BiFunction nicht. Daher sollten Sie Func auch mit einem Argument und Func mit zwei Argumenten definieren. In .NET würden zirkuläre Abhängigkeiten mithilfe von Erweiterungsmethoden umgangen, die in Java nicht vorhanden sind.

Hans
quelle
2
Warum brauchen Sie andThenmit 16 Argumenten? Das Ergebnis einer Funktion in Java ist ein einzelner Wert. andThennimmt diesen Wert und macht etwas damit. Es gibt auch kein Problem mit der Benennung. Klassennamen sollten unterschiedlich sein und sich in verschiedenen Dateien mit demselben Namen befinden - gemäß der Logik, die von Java-Sprachentwicklern mit Function und BiFunction festgelegt wurde. Alle diese unterschiedlichen Namen werden auch benötigt, wenn die Argumenttypen unterschiedlich sind. Man kann einen VargFunction(T, R) { R apply(T.. t) ... }für einen einzigen Typ erstellen .
Alex Pakka
2

Ich habe den Quellcode für BiFunction hier gefunden:

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

Ich habe es geändert, um TriFunction zu erstellen. Wie BiFunction verwendet es andThen () und nicht compose (). Daher ist es für einige Anwendungen, für die compose () erforderlich ist, möglicherweise nicht geeignet. Es sollte für normale Arten von Objekten in Ordnung sein. Einen guten Artikel zu andThen () und compose () finden Sie hier:

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
quelle
2

Sie können auch Ihre eigene Funktion mit den 3 Parametern erstellen

@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
quelle
0

Sie können nicht immer bei TriFunction anhalten. Manchmal müssen Sie möglicherweise n Parameter an Ihre Funktionen übergeben. Dann muss das Support-Team eine QuadFunction erstellen, um Ihren Code zu reparieren. Eine langfristige Lösung wäre, ein Objekt mit den zusätzlichen Parametern zu erstellen und dann die vorgefertigte Funktion oder BiFunktion zu verwenden.

Koushik Roy
quelle