Java 8: Gdzie jest TriFunction (i krewni) w java.util.function? Albo jaka jest alternatywa?

113

Widzę java.util.function.BiFunction, więc mogę to zrobić:

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

A jeśli to nie wystarczy i potrzebuję TriFunction? To nie istnieje!

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

Chyba powinienem dodać, że wiem, że potrafię zdefiniować własną TriFunction, po prostu próbuję zrozumieć powody, dla których nie uwzględniono jej w standardowej bibliotece.

Richard Finegan
źródło
1
dzięki interfejsowi bifunkcyjnemu można łatwo zdefiniować klasę funkcji N, jeśli zdefiniujesz funkcję trójfunkcyjną jako oddzielny interfejs, najpierw sb zapyta, dlaczego nie czterofunkcyjny, a po drugie, musisz zduplikować wszystkie metody, które przyjmują Bfunction jako parametr
user902383
6
W przypadku takich interfejsów API istnieje punkt malejących korzyści. (Osobiście myślę, że JDK8 przeszedł to jakiś czas temu, ale to jest jeszcze więcej.)
Louis Wasserman,
Uważam, że uzasadnione było stwierdzenie, że Function i BiFunction zostały w pełni zaimplementowane z obiektami i typami natywnymi. Włączenie TriFunkcji ze wszystkimi różnymi odmianami wysadziłoby środowisko JRE klasami i metodami.
Thorbjørn Ravn Andersen
1
Krótka odpowiedź. W Javie, jeśli go nie widzisz, budujesz własne (patrz oczywiście odpowiedzi Alex P). Na marginesie, w C # implementatorzy dotnet dali gotowe gotowe (do 16 argumentów), ale bez nazw prefiksów („Bi” tutaj): zobacz docs.microsoft.com/en-us/dotnet/api/ ... Po prostu prosta funkcja „Func”. Jest to więc jedno z miejsc, w których wolę dotnet od Java. Proszę nie zamieniać tej sekcji komentarzy w wojnę na halo. i ogranicz komentarze tylko do BiFunction.
granadaCoder

Odpowiedzi:

81

O ile wiem, istnieją tylko dwa rodzaje funkcji, destrukcyjne i konstruktywne.

Podczas gdy funkcja konstruktywna, jak sama nazwa wskazuje, coś konstruuje, to destrukcyjna coś niszczy, ale nie w taki sposób, w jaki myślisz teraz.

Na przykład function

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

jest konstruktywna . Ponieważ musisz coś skonstruować. W tym przykładzie utworzyłeś krotkę (x, y) . Funkcje konstruktywne mają problem polegający na tym, że nie są w stanie obsłużyć nieskończonych argumentów. Ale najgorsze jest to, że nie możesz po prostu pozostawić otwartej kłótni. Nie możesz po prostu powiedzieć „no cóż, niech x: = 1” i wypróbować wszystkie y, które chcesz spróbować. Za każdym razem musisz utworzyć całą krotkę z x := 1. Więc jeśli chcesz zobaczyć, co zwracają funkcje y := 1, y := 2, y := 3, musisz napisać f(1,1) , f(1,2) , f(1,3).

W Javie 8 funkcje konstruktywne powinny być obsługiwane (przez większość czasu) przy użyciu odwołań do metod, ponieważ nie ma zbytniej korzyści z używania konstruktywnej funkcji lambda. Są trochę jak metody statyczne. Możesz ich używać, ale nie mają rzeczywistego stanu.

Drugi typ jest destrukcyjny, zabiera coś i demontuje w razie potrzeby. Na przykład funkcja destrukcyjna

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

robi to samo, co funkcja, fktóra była konstruktywna. Zaletą funkcji destrukcyjnej jest to, że możesz teraz obsługiwać nieskończone argumenty, co jest szczególnie wygodne w przypadku strumieni, i możesz po prostu pozostawić argumenty otwarte. Więc jeśli znowu chcesz zobaczyć, jaki byłby wynik, jeśli x := 1i y := 1 , y := 2 , y := 3, możesz powiedzieć h = g(1)i h(1)jest wynikiem dla y := 1, h(2)dla y := 2i h(3)dla y := 3.

Więc tutaj masz ustalony stan! To dość dynamiczne i przez większość czasu to jest to, czego oczekujemy od lambdy.

Wzorce takie jak Factory są o wiele łatwiejsze, jeśli możesz po prostu wstawić funkcję, która wykonuje pracę za Ciebie.

Niszczycielskie można łatwo łączyć ze sobą. Jeśli typ jest odpowiedni, możesz po prostu skomponować je według własnego uznania. Używając tego, możesz łatwo zdefiniować morfizmy, które znacznie ułatwiają testowanie (przy niezmiennych wartościach)!

Możesz to też zrobić konstruktywną, ale destrukcyjna kompozycja wygląda ładniej i bardziej przypomina listę lub dekorator, a konstruktywna bardzo przypomina drzewo. A rzeczy takie jak wycofywanie się z funkcjami konstruktywnymi są po prostu nieprzyjemne. Możesz po prostu zapisać częściowe funkcje destrukcyjnej (programowanie dynamiczne), a na "backtrack" po prostu użyć starej funkcji destrukcyjnej. To sprawia, że ​​kod jest dużo mniejszy i bardziej czytelny. Dzięki funkcjom konstruktywnym masz mniej więcej do zapamiętania wszystkich argumentów, których może być dużo.

Dlaczego więc istnieje potrzeba, aby BiFunctionbyć bardziej kwestionowanym, niż dlaczego nie ma TriFunction?

Po pierwsze, przez wiele czasu masz po prostu kilka wartości (mniej niż 3) i potrzebujesz tylko wyniku, więc normalna funkcja niszcząca nie byłaby w ogóle potrzebna, konstruktywna byłaby w porządku. Są rzeczy takie jak monady, które naprawdę potrzebują konstruktywnej funkcji. Ale poza tym nie ma zbyt wielu dobrych powodów, dla których w ogóle istnieje BiFunction. Co nie oznacza, że ​​należy go usunąć! Walczę o moje monady, aż umrę!

Więc jeśli masz wiele argumentów, których nie możesz połączyć w logiczną klasę kontenera, i jeśli chcesz, aby funkcja była konstruktywna, użyj odwołania do metody. W przeciwnym razie spróbuj użyć nowo zdobytej zdolności funkcji niszczących, może się okazać, że robisz wiele rzeczy z dużo mniejszą liczbą linii kodu.

user3003859
źródło
2
Odpowiedziałeś na moje pytanie ... Myślę ... Nie wiem, czy projektanci języka Java wywodzą się z tego sposobu myślenia, ale nie jestem dobrze zorientowany w programowaniu funkcjonalnym. Dziękuję za wyjaśnienie.
Richard Finegan,
81
Nigdy nie widziałem, aby terminy konstruktywne i destrukcyjne były używane w odniesieniu do koncepcji, które opisujesz. Myślę, że curry i non-curried są bardziej powszechnymi terminami.
Feuermurmel
17
Pierwszy przykład funkcji nie jest poprawny składniowo. Powinien to być BiFunction, a nie Function, ponieważ przyjmuje dwa argumenty wejściowe.
annouk
3
IMO BiFunctionzostała stworzona, aby umożliwić łatwą redukcję danych, a większość Streamoperacji terminalowych to tylko redukcja danych. Dobry przykład jest BinaryOperator<T>używany w wielu Collectors. Pierwszy element jest redukowany za pomocą drugiego, a następnie można go redukować następnym i tak dalej. Oczywiście możesz utworzyć Function<T, Function<T, T>func = x -> (y -> / * kod redukcji tutaj * /). Ale poważnie? Wszystko to, gdy możesz po prostu zrobić BinaryOperator<T> func = (x, y) -> /*reduction code here*/. Poza tym to podejście do redukcji danych wydaje mi się bardzo podobne do twojego „destrukcyjnego” podejścia.
FBB
32
Jak to się stało, że zebrało tyle głosów pozytywnych? To okropna i myląca odpowiedź, ponieważ opiera się na założeniu, że Function<Integer,Integer> f = (x,y) -> x + yjest prawidłowa Java, a nią nie jest. Na początek powinna to być funkcja BiFunction!
wvdz
162

Jeśli potrzebujesz TriFunction, zrób to:

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

Poniższy mały program pokazuje, jak można go używać. Pamiętaj, że typ wyniku jest określony jako ostatni parametr typu ogólnego.

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

Wydaje mi się, że gdyby TriFunction miał praktyczne zastosowanie, java.util.*czy java.lang.*zostałby zdefiniowany. Nigdy nie przekroczyłbym jednak 22 argumentów ;-) Co przez to rozumiem, cały nowy kod, który umożliwia strumieniowe przesyłanie kolekcji, nigdy nie wymagał TriFunction jako żadnego z parametrów metody. Więc to nie zostało uwzględnione.

AKTUALIZACJA

Aby uzyskać kompletność i po wyjaśnieniu destrukcyjnych funkcji w innej odpowiedzi (związanej z curry), oto jak można emulować TriFunction bez dodatkowego interfejsu:

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

Oczywiście istnieje możliwość łączenia funkcji na inne sposoby, np .:

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

Chociaż currying byłby naturalny dla każdego języka, który obsługuje programowanie funkcjonalne poza lambdami, Java nie jest zbudowana w ten sposób i chociaż jest to możliwe, kod jest trudny do utrzymania, a czasem do odczytania. Jest to jednak bardzo pomocne jako ćwiczenie, a czasami funkcje częściowe mają należne miejsce w kodzie.

Alex Pakka
źródło
6
Dzięki za rozwiązanie. I tak, na pewno jest zastosowanie dla BiFunction, TriFunction, ... W przeciwnym razie ludzie by go nie szukali. Wygląda na to, że cała kwestia lambda jest teraz zbyt nowa dla Oracle i zostanie rozszerzona w późniejszych wersjach Javy. W tej chwili jest to bardziej dowód słuszności koncepcji.
Stefan Endrullis,
Hy @Alex, czy możesz zdefiniować następującą linię. co się tutaj dzieje 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 - pozwala na tworzenie kompozycji funkcji: 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); //12Zobacz stackoverflow.com/questions/19834611/…
Alex Pakka
Dodano andThen()przykład użycia do odpowiedzi.
Alex Pakka,
Nie tylko curry nie jest dobrze przystosowane do języka Java, ale także popraw mnie, jeśli się mylę, ale BiFunctionjest używane w StreamAPI do redukcji danych, co wygląda bardzo podobnie do mojego podejścia do curry: nigdy nie bierzesz więcej niż dwa argumenty i możesz przetwarzać dowolną liczbę elementów, po jednej redukcji na raz (zobacz mój komentarz do zaakceptowanej odpowiedzi, byłbym szczęśliwy, gdyby się mylę, widząc to w ten sposób).
FBB
13

Alternatywą jest dodanie poniższej zależności,

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

Teraz możesz użyć funkcji Vavr, jak poniżej, do 8 argumentów,

3 argumenty:

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

5 argumentów:

Function5<Integer, Integer, Integer, Integer, Integer, Integer> f = 
      (a, b, c, d, e) -> a + b + c + d + e;
Amol Damodar
źródło
2
Miałem właśnie zaktualizować swoją odpowiedź, aby wspomnieć o vavr, ale byłeś pierwszy, więc zagłosowałem za. Jeśli dojdziesz do punktu, w którym potrzebujesz TriFunction, istnieje duża szansa, że ​​skorzystasz z vavrbiblioteki - to sprawia, że ​​programowanie w stylu funkcjonalnym jest tak znośne w Javie, jak to tylko możliwe.
Alex Pakka
7

Mam prawie to samo pytanie i częściową odpowiedź. Nie jestem pewien, czy konstruktywna / dekonstrukcyjna odpowiedź jest tym, co mieli na myśli projektanci języka. Myślę, że posiadanie 3 i więcej do N ma ważne przypadki użycia.

Pochodzę z .NET. aw .NET masz Func i Action dla funkcji void. Istnieją również orzeczenia i inne przypadki specjalne. Zobacz: https://msdn.microsoft.com/en-us/library/bb534960(v=vs.110).aspx

Zastanawiam się, jaki był powód, dla którego projektanci języka zdecydowali się na Function, Bfunction i nie kontynuowali do DecaExiFunction?

Odpowiedzią na drugą część jest wymazywanie typów. Po kompilacji nie ma różnicy między Func i Func. W związku z tym nie kompiluje się:

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

Wewnętrzne funkcje zostały wykorzystane do obejścia innego drobnego problemu. Eclipse nalegał na umieszczenie obu klas w plikach o nazwie Function w tym samym katalogu ... Nie jestem pewien, czy jest to obecnie problem kompilatora. Ale nie mogę zmienić błędu w Eclipse.

Func został użyty, aby zapobiec konfliktom nazw z typem funkcji Java.

Więc jeśli chcesz dodać Func od 3 do 16 argumentów, możesz zrobić dwie rzeczy.

  • Twórz TriFunc, TesseraFunc, PendeFunc, ... DecaExiFunc itp
    • (Powinienem używać greki czy łaciny?)
  • Użyj nazw pakietów lub klas, aby nazwy były różne.

Przykład drugiego sposobu:

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

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

i

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

Jakie byłoby najlepsze podejście?

W powyższych przykładach nie uwzględniłem implementacji metod andThen () i compose (). Jeśli dodasz te, musisz dodać 16 przeciążeń każde: TriFunc powinien mieć andthen () z 16 argumentami. To spowodowałoby błąd kompilacji z powodu zależności cyklicznych. Nie miałbyś również tych przeciążeń dla funkcji i BiFunction. Dlatego powinieneś także zdefiniować Func z jednym argumentem i Func z dwoma argumentami. W .NET zależności cykliczne można by obejść za pomocą metod rozszerzających, które nie są obecne w Javie.

Hans
źródło
2
Dlaczego miałbyś potrzebować andThen16 argumentów? Wynikiem funkcji w Javie jest pojedyncza wartość. andThenprzyjmuje tę wartość i coś z nią robi. Nie ma też problemu z nazewnictwem. Nazwy klas powinny być różne i znajdować się w różnych plikach o tej samej nazwie - zgodnie z logiką ustaloną przez programistów języka Java z funkcjami Function i BiFunction. Ponadto wszystkie te różne nazwy są potrzebne, jeśli typy argumentów są różne. Można utworzyć VargFunction(T, R) { R apply(T.. t) ... }dla jednego typu.
Alex Pakka,
2

Znalazłem kod źródłowy BiFunction tutaj:

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

Zmodyfikowałem go, aby utworzyć TriFunction. Podobnie jak BiFunction, używa andThen (), a nie compose (), więc w przypadku niektórych aplikacji, które wymagają compose (), może to nie być odpowiednie. Powinien być odpowiedni dla zwykłych obiektów. Dobry artykuł na temat andThen () i compose () można znaleźć tutaj:

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
źródło
2

Możesz także utworzyć własną funkcję, korzystając z 3 parametrów

@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
źródło
1

Nie możesz zawsze zatrzymać się w TriFunction. Czasami może być konieczne przekazanie n liczby parametrów do funkcji. Następnie zespół pomocy technicznej będzie musiał stworzyć QuadFunction, aby naprawić Twój kod. Długoterminowym rozwiązaniem byłoby utworzenie obiektu z dodatkowymi parametrami, a następnie użycie gotowej funkcji lub funkcji BiFunction.

Koushik Roy
źródło