Java 8: Onde está TriFunction (e kin) em java.util.function? Ou qual é a alternativa?

113

Vejo java.util.function.BiFunction, então posso fazer isso:

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

E se isso não for bom o suficiente e eu precisar do TriFunction? Não existe!

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

Acho que devo acrescentar que sei que posso definir meu próprio TriFunction, estou apenas tentando entender a razão por trás de não incluí-lo na biblioteca padrão.

Richard Finegan
fonte
1
com interface bifuncional, você pode definir facilmente a classe de função N, se você definir trifunção como interface separada, primeiro sb perguntará por que não quadofunção, e segundo, você precisa duplicar todos os métodos que levam Bifunção como parâmetro
user902383
6
Há um ponto de diminuição dos retornos para APIs como essa. (Pessoalmente, acho que o JDK8 passou há um tempo, mas isso está além disso.)
Louis Wasserman
Acredito que a lógica era dizer que Function e BiFunction foram totalmente implementadas com objetos e tipos nativos. Incluir TriFunctions com todas as várias variações explodiria o JRE com classes e métodos.
Thorbjørn Ravn Andersen
1
Resposta curta. Em Java, se você não o vê, você constrói o seu próprio (veja as respostas de Alex P, é claro). Sidenote, em C #, os implementadores do dotnet deram a você alguns predefinidos (até 16 argumentos), mas sem os nomes de prefixo ("Bi" aqui): consulte docs.microsoft.com/en-us/dotnet/api/… Apenas um simples "Func". Portanto, este é um dos lugares que eu prefiro dotnet em vez de java. Por favor, não transforme esta seção de comentários em uma guerra sagrada. e limitar os comentários apenas a BiFunction.
granadaCoder

Respostas:

81

Pelo que eu sei, existem apenas dois tipos de funções, destrutivas e construtivas.

Enquanto a função construtiva, como o nome indica, constrói algo, uma função destrutiva destrói algo, mas não da maneira que você pode pensar agora.

Por exemplo, a função

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

é construtivo . Como você precisa construir algo. No exemplo, você construiu a tupla (x, y) . As funções construtivas têm o problema de não serem capazes de lidar com argumentos infinitos. Mas o pior é que você não pode simplesmente deixar uma discussão aberta. Você não pode simplesmente dizer "bem, deixe x: = 1" e experimentar todos os y que desejar. Você tem que construir a cada vez toda a tupla com x := 1. Então, se você quiser ver o que as funções retornam, y := 1, y := 2, y := 3você precisa escrever f(1,1) , f(1,2) , f(1,3).

No Java 8, as funções construtivas devem ser tratadas (na maioria das vezes) usando referências de método porque não há muita vantagem em usar uma função lambda construtiva. Eles são um pouco como métodos estáticos. Você pode usá-los, mas eles não têm estado real.

O outro tipo é o destrutivo, pega algo e desmonta na medida do necessário. Por exemplo, a função destrutiva

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

faz o mesmo que a função fque foi construtiva. Os benefícios de uma função destrutiva são: agora você pode lidar com infinitos argumentos, o que é especialmente conveniente para fluxos, e pode simplesmente deixar os argumentos abertos. Portanto, se você quiser ver novamente como seria o resultado se x := 1e y := 1 , y := 2 , y := 3, você pode dizer h = g(1)e h(1)é o resultado para y := 1, h(2)para y := 2e h(3)para y := 3.

Então aqui você tem um estado fixo! Isso é bastante dinâmico e na maioria das vezes é o que queremos de um lambda.

Padrões como o Factory são muito mais fáceis se você puder apenas incluir uma função que faça o trabalho para você.

Os destrutivos são facilmente combinados uns com os outros. Se o tipo estiver certo, você pode apenas compor como quiser. Usando isso, você pode definir facilmente morfismos que tornam (com valores imutáveis) o teste muito mais fácil!

Você também pode fazer isso com uma composição construtiva, mas a composição destrutiva parece mais bonita e mais parecida com uma lista ou um decorador, e a construtiva se parece muito com uma árvore. E coisas como voltar atrás com funções construtivas simplesmente não são legais. Você pode apenas salvar as funções parciais de uma destrutiva (programação dinâmica), e no "retrocesso" apenas usar a função destrutiva antiga. Isso torna o código muito menor e mais legível. Com funções construtivas, você tem mais ou menos para se lembrar de todos os argumentos, o que pode ser muito.

Então, por que é necessário BiFunctionhaver mais perguntas do que por que não há TriFunction?

Primeiro de tudo, muito tempo você tem apenas alguns valores (menos de 3) e precisa apenas de um resultado, então uma função destrutiva normal não seria necessária, uma função construtiva seria suficiente. E há coisas como mônadas que realmente precisam de uma função construtiva. Mas, fora isso, não há realmente muitas boas razões para haver um BiFunction. O que não significa que deva ser removido! Eu luto por minhas Mônadas até morrer!

Portanto, se você tiver muitos argumentos, que não podem ser combinados em uma classe de contêiner lógica, e se precisar que a função seja construtiva, use uma referência de método. Caso contrário, tente usar a nova habilidade adquirida de funções destrutivas, você pode se ver fazendo muitas coisas com muito menos linhas de código.

user3003859
fonte
2
Você respondeu minha pergunta ... Eu acho ... Eu não sei se os designers da linguagem java estão vindo dessa linha de pensamento, mas não sou muito versado em programação funcional. Obrigado pela explicação.
Richard Finegan
79
Nunca vi os termos construtivo e destrutivo sendo usados ​​para se referir aos conceitos que você descreve. Acho que curry e não curry são termos mais comuns.
Feuermurmel
17
O primeiro exemplo de função não está sintaticamente correto. Deve ser BiFunction e não Function, porque leva dois argumentos de entrada.
aniversário de
3
O IMO BiFunctionfoi criado para permitir uma redução fácil de dados e a maioria das Streamoperações de terminal são apenas reduções de dados. Um bom exemplo é BinaryOperator<T>, usado em muitos Collectors. Um primeiro elemento é reduzido com o segundo, que pode então ser reduzido com o próximo, e assim por diante. Claro, você pode criar um Function<T, Function<T, T>func = x -> (y -> / * código de redução aqui * /). Mas seriamente? Tudo isso quando você pode simplesmente fazer BinaryOperator<T> func = (x, y) -> /*reduction code here*/. Além disso, essa abordagem de redução de dados se parece muito com sua abordagem "destrutiva" para mim.
FBB
32
Como isso conseguiu tantos votos positivos? É uma resposta terrível e confusa, porque se baseia na premissa de que Function<Integer,Integer> f = (x,y) -> x + yJava é válido, o que não é. Isso deveria ser um BiFunction para começar!
wvdz
162

Se você precisar do TriFunction, basta fazer o seguinte:

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

A seguir, um pequeno programa mostra como ele pode ser usado. Lembre-se de que o tipo de resultado é especificado como um ú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]
    }
  }

Eu acho que se houvesse um uso prático para TriFunction em java.util.*ou java.lang.*ele teria sido definido. Eu nunca iria além de 22 argumentos ;-) O que quero dizer com isso, todo código novo que permite fazer streaming de coleções nunca exigiu TriFunction como qualquer um dos parâmetros do método. Portanto, não foi incluído.

ATUALIZAR

Para completar e seguir a explicação das funções destrutivas em outra resposta (relacionada ao currying), veja como o TriFunction pode ser emulado sem interface 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

Claro, é possível combinar funções de outras maneiras, por exemplo:

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

Embora currying seja natural para qualquer linguagem que suporte programação funcional além de lambdas, Java não é construído dessa forma e, embora seja possível, o código é difícil de manter e às vezes de ler. No entanto, é muito útil como exercício e, às vezes, funções parciais têm um lugar legítimo em seu código.

Alex Pakka
fonte
6
Obrigado pela solução. E sim, definitivamente há uso para BiFunction, TriFunction, ... Caso contrário, as pessoas não iriam procurá-lo. Acho que toda a coisa lambda é muito nova para a Oracle agora e será estendida em versões posteriores do Java. No momento, é mais uma prova de conceito.
Stefan Endrullis
Hy @Alex você pode definir a seguinte linha. o que está acontecendo aqui padrão <V> TriFunction <A, B, C, V> e então (Função <? super R,? extends V> após) {Objects.requireNonNull (após); return (A a, B b, C c) -> after.apply (apply (apply (a, b, c)); }
Muneeb Nasir
@MuneebNasir - permite que você faça a composição de funções: 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
Adicionado andThen()exemplo de uso à resposta.
Alex Pakka
Não só currying não é bem adaptado para a linguagem Java, mas também, corrija-me se eu estiver errado, mas BiFunctioné usado na StreamAPI para realizar redução de dados, que se parece muito com a abordagem currying para mim: você nunca toma mais do que dois argumentos, e você pode processar qualquer número de elementos, uma redução de cada vez (veja meu comentário sobre a resposta aceita, ficaria feliz em saber se estou errado em ver dessa forma).
FBB
13

A alternativa é adicionar a dependência abaixo,

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

Agora, você pode usar a função Vavr, como abaixo de até 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
fonte
2
Eu estava prestes a atualizar minha resposta para mencionar vavr, mas você foi o primeiro, então votei a favor. Se você chegar ao ponto de que precisa de um TriFunction, há uma grande chance de você se sair melhor usando a vavrbiblioteca - ela torna a programação de estilo funcional o mais suportável possível em Java.
Alex Pakka 01 de
7

Tenho quase a mesma pergunta e uma resposta parcial. Não tenho certeza se a resposta construtiva / desconstrutiva é o que os designers da linguagem tinham em mente. Acho que ter 3 e mais até N tem casos de uso válidos.

Eu venho do .NET. e no .NET você tem Func e Action para funções void. Predicado e alguns outros casos especiais também existem. Consulte: https://msdn.microsoft.com/en-us/library/bb534960(v=vs.110).aspx

Eu me pergunto qual foi a razão pela qual os designers de linguagem optaram por Function, Bifunction e não continuaram até DecaExiFunction?

A resposta à segunda parte é o apagamento de tipo. Após a compilação, não há diferença entre Func e Func. Portanto, o seguinte não 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);
    }
}

Funções internas foram usadas para contornar outro problema menor. O Eclipse insistiu em ter ambas as classes em arquivos denominados Function no mesmo diretório ... Não tenho certeza se isso é um problema do compilador hoje em dia. Mas não posso virar o erro de no Eclipse.

Func foi usado para evitar conflitos de nome com o tipo de Função java.

Portanto, se você deseja adicionar Func de 3 a 16 argumentos, pode fazer duas coisas.

  • Faça TriFunc, TesseraFunc, PendeFunc, ... DecaExiFunc etc
    • (Devo usar grego ou latim?)
  • Use nomes de pacotes ou classes para tornar os nomes diferentes.

Exemplo para a segunda maneira:

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

Qual seria a melhor abordagem?

Nos exemplos acima, não incluí implementações para os métodos andThen () e compose (). Se você adicionar esses, deve adicionar 16 sobrecargas cada: o TriFunc deve ter um andthen () com 16 argumentos. Isso geraria um erro de compilação devido às dependências circulares. Além disso, você não teria essas sobrecargas para Function e BiFunction. Portanto, você também deve definir Func com um argumento e Func com dois argumentos. Em .NET, as dependências circulares seriam contornadas pelo uso de métodos de extensão que não estão presentes em Java.

Hans
fonte
2
Por que você precisaria andThencom 16 argumentos? O resultado de uma função em Java é um valor único. andThenpega esse valor e faz algo com ele. Além disso, não há problema com a nomenclatura. Os nomes das classes devem ser diferentes e estar em arquivos diferentes com o mesmo nome - seguindo a lógica definida pelos desenvolvedores da linguagem Java com Function e BiFunction. Além disso, todos esses nomes diferentes são necessários se os tipos de argumento forem diferentes. Pode-se criar um VargFunction(T, R) { R apply(T.. t) ... }único tipo.
Alex Pakka
2

Encontrei o código-fonte do BiFunction aqui:

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

Eu modifiquei para criar o TriFunction. Como BiFunction, ele usa andThen () e não compose (), portanto, para alguns aplicativos que requerem compose (), pode não ser apropriado. Deve servir para tipos normais de objetos. Um bom artigo sobre andThen () e compose () pode ser encontrado aqui:

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

Você também pode criar sua própria função usando os 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
fonte
0

Você nem sempre pode parar no TriFunction. Às vezes, você pode precisar passar n número de parâmetros para suas funções. Então a equipe de suporte terá que criar um QuadFunction para consertar seu código. A solução de longo prazo seria criar um Objeto com os parâmetros extras e, em seguida, usar a Função ou BiFunção pronta.

Koushik Roy
fonte