Java 8: где TriFunction (и другие) в java.util.function? Или какая альтернатива?

113

Я вижу java.util.function.BiFunction, поэтому могу сделать следующее:

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

Что, если этого недостаточно и мне нужна TriFunction? Его не существует!

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

Думаю, мне следует добавить, что я знаю, что могу определить свою собственную TriFunction, я просто пытаюсь понять причину отказа от включения ее в стандартную библиотеку.

Ричард Финеган
источник
1
с бифункциональным интерфейсом вы можете легко определить класс N-функций, если вы определите трифункцию как отдельный интерфейс, сначала sb спросит, почему бы не использовать четырехфункцию, а во-вторых, вам нужно продублировать все методы, которые принимают бифункцию в качестве параметра
user902383
6
Есть точка уменьшения отдачи от подобных API. (Лично я думаю, что JDK8 прошел его некоторое время назад, но это выходит за рамки даже этого.)
Луи Вассерман
Я считаю, что смысл был в том, чтобы сказать, что Function и BiFunction были полностью реализованы с объектами и собственными типами. Включение TriFunctions со всеми различными вариациями взорвет JRE классами и методами.
Thorbjørn Ravn Andersen
1
Короткий ответ. В Java, если вы этого не видите, вы создаете свой собственный (см., Конечно, ответы Alex P). Заметка: в C # разработчики dotnet предоставили вам заранее подготовленные аргументы (до 16 аргументов), но без префиксов (здесь «Bi»): см. Docs.microsoft.com/en-us/dotnet/api/… Просто простой "Func". Так что это одно из мест, где я предпочитаю dotnet над java. Пожалуйста, не превращайте этот раздел комментариев в настоящую войну. и ограничивайте комментарии только BiFunction.
granadaCoder

Ответы:

81

Насколько мне известно, есть только два вида функций: деструктивные и конструктивные.

В то время как конструктивная функция, как следует из названия, что-то конструирует, деструктивная функция что-то разрушает, но не так, как вы сейчас думаете.

Например, функция

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

является конструктивным . Как вам нужно что-то построить. В этом примере вы построили кортеж (x, y) . У конструктивных функций есть проблема, заключающаяся в том, что они не могут обрабатывать бесконечные аргументы. Но хуже всего то, что нельзя просто оставить спор открытым. Вы не можете просто сказать «ну, пусть x: = 1» и пробовать каждый y, какой захотите. Вы должны каждый раз строить весь кортеж с помощью x := 1. Так что, если вы хотите увидеть, что возвращают функции, y := 1, y := 2, y := 3вы должны написать f(1,1) , f(1,2) , f(1,3).

В Java 8 конструктивные функции должны обрабатываться (большую часть времени) с использованием ссылок на методы, потому что использование конструктивной лямбда-функции не дает особых преимуществ. Они немного похожи на статические методы. Вы можете использовать их, но они не имеют реального состояния.

Другой тип - разрушительный, он что-то берет и разбирает по мере необходимости. Например, деструктивная функция

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

делает то же самое, что и функция, fкоторая была конструктивной. Преимущества деструктивной функции заключаются в том, что теперь вы можете обрабатывать бесконечные аргументы, что особенно удобно для потоков, и вы можете просто оставить аргументы открытыми. Итак, если вы снова захотите увидеть, каков будет результат, если x := 1и y := 1 , y := 2 , y := 3, вы можете сказать h = g(1)и h(1)является результатом для y := 1, h(2)для y := 2и h(3)для y := 3.

Итак, у вас есть фиксированное состояние! Это довольно динамично, и в большинстве случаев это именно то, что мы хотим от лямбды.

Такие шаблоны, как Factory, намного проще, если вы можете просто добавить функцию, которая сделает всю работу за вас.

Деструктивные легко сочетаются друг с другом. Если тип правильный, вы можете просто составить их, как вам нравится. Используя это, вы можете легко определять морфизмы, которые значительно упрощают (с неизменяемыми значениями) тестирование!

Вы можете сделать то же самое с конструктивным, но деструктивная композиция выглядит лучше и больше похожа на список или декоратор, а конструктивная очень похожа на дерево. А такие вещи, как возврат с помощью конструктивных функций, просто нехорошие. Вы можете просто сохранить частичные функции деструктивного (динамическое программирование), а при «возврате» просто использовать старую деструктивную функцию. Это делает код намного меньше и лучше читается. С конструктивными функциями у вас есть более или менее запомнить все аргументы, которых может быть много.

Итак, почему существует необходимость, BiFunctionдолжно быть больше вопросов, чем почему нет TriFunction?

Во-первых, много времени у вас есть всего несколько значений (меньше 3) и нужен только результат, поэтому обычная деструктивная функция не понадобится вообще, а конструктивная подойдет. И есть такие вещи, как монады, которые действительно нуждаются в конструктивной функции. Но помимо этого, на самом деле не так много веских причин, по которым вообще существует BiFunction. Это не значит, что его нужно удалить! Я борюсь за свои Монады, пока не умру!

Поэтому, если у вас много аргументов, которые нельзя объединить в логический контейнерный класс, и если вам нужно, чтобы функция была конструктивной, используйте ссылку на метод. В противном случае попробуйте использовать новую полученную способность деструктивных функций, вы можете обнаружить, что делаете много вещей с гораздо меньшим количеством строк кода.

пользователь3003859
источник
2
Вы ответили на мой вопрос ... Я думаю ... Я не знаю, исходят ли разработчики языка java из этой линии мышления, но я плохо разбираюсь в функциональном программировании. Спасибо за объяснение.
Ричард Финеган
81
Я никогда не видел, чтобы термины « конструктивный» и « деструктивный» использовались для обозначения описываемых вами концепций. Я думаю, что карри и не карри - более распространенные термины.
Feuermurmel 03
17
Первый пример функции синтаксически неверен. Это должна быть BiFunction, а не Function, потому что она принимает два входных аргумента.
Annouk
3
IMO BiFunctionбыл создан, чтобы упростить сокращение данных, и большинство Streamопераций терминала - это просто сокращение данных. Хороший пример BinaryOperator<T>используется во многих Collectors. Первый элемент сокращается с помощью второго, затем может быть уменьшен с помощью следующего, и так далее. Конечно, вы можете создать здесь Function<T, Function<T, T>func = x -> (y -> / * код сокращения * /). Но серьезно? Все это когда можно просто сделать BinaryOperator<T> func = (x, y) -> /*reduction code here*/. Кроме того, этот подход к сокращению данных мне очень напоминает ваш «деструктивный» подход.
FBB
32
Как это получило столько голосов? Это ужасный и сбивающий с толку ответ, потому что он основан на предположении, что Function<Integer,Integer> f = (x,y) -> x + yJava является допустимой, а это не так. Для начала это должна быть BiFunction!
wvdz
162

Если вам нужна TriFunction, просто сделайте это:

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

Следующая небольшая программа показывает, как ее можно использовать. Помните, что тип результата указывается в качестве последнего параметра универсального типа.

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

Я думаю , если есть практическое применение TriFunction в java.util.*или java.lang.*она была бы определена. Однако я бы никогда не пошел дальше 22 аргументов ;-) Что я имею в виду под этим, весь новый код, который позволяет передавать коллекции, никогда не требовал TriFunction в качестве любого из параметров метода. Так что это не было включено.

ОБНОВИТЬ

Для полноты и следования объяснению деструктивных функций в другом ответе (связанном с каррированием), вот как можно эмулировать TriFunction без дополнительного интерфейса:

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

Конечно, можно комбинировать функции и другими способами, например:

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

Хотя каррирование было бы естественным для любого языка, который поддерживает функциональное программирование помимо лямбда-выражений, Java не построена таким образом, и, хотя это достижимо, код трудно поддерживать, а иногда и читать. Однако это очень полезно в качестве упражнения, и иногда частичные функции занимают достойное место в вашем коде.

Алекс Пакка
источник
6
Спасибо за решение. И да, использование BiFunction, TriFunction, ... Иначе люди не стали бы это искать. Предполагается, что сама лямбда-функция слишком нова для Oracle прямо сейчас и будет расширена в более поздних версиях Java. На данный момент это скорее проверка концепции.
Стефан Эндруллис
Hy @Alex, не могли бы вы определить следующую строку. что здесь происходит по умолчанию <V> TriFunction <A, B, C, V> andThen (Функция <? super R,? extends V> after) {Objects.requireNonNull (after); return (A a, B b, C c) -> after.apply (применить (a, b, c)); }
Muneeb Nasir
@MuneebNasir - он позволяет вам выполнять композицию функций: 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); //12см. Stackoverflow.com/questions/19834611/…
Alex Pakka
К andThen()ответу добавлен пример использования.
Alex Pakka
Каррирование не только плохо адаптировано к языку Java, но и, поправьте меня, если я ошибаюсь, BiFunctionоно используется в StreamAPI для сокращения данных, что очень похоже на подход каррирования для меня: вы никогда не берете больше двух аргументы, и вы можете обрабатывать любое количество элементов, по одному сокращению за раз (см. мой комментарий к принятому ответу, я был бы рад узнать, ошибаюсь ли я в этом).
FBB
13

Альтернативой является добавление приведенной ниже зависимости,

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

Теперь вы можете использовать функцию Vavr, как показано ниже, до 8 аргументов,

3 аргумента:

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

5 аргументов:

Function5<Integer, Integer, Integer, Integer, Integer, Integer> f = 
      (a, b, c, d, e) -> a + b + c + d + e;
Амол Дамодар
источник
2
Я собирался обновить свой ответ, чтобы упомянуть vavr, но вы были первым, поэтому я проголосовал за. Если вы дойдете до того, что вам нужна TriFunction, есть большая вероятность, что вам будет лучше, если вы воспользуетесь vavrбиблиотекой - это делает программирование в функциональном стиле на Java настолько терпимым, насколько это возможно.
Alex
7

У меня почти такой же вопрос и частичный ответ. Не уверен, что конструктивный / деконструктивный ответ имел в виду разработчики языка. Я думаю, что у 3 и более до N есть допустимые варианты использования.

Я из .NET. а в .NET у вас есть Func и Action для функций void. Также существуют предикаты и некоторые другие частные случаи. См. Https://msdn.microsoft.com/en-us/library/bb534960(v=vs.110).aspx

Интересно, по какой причине разработчики языка выбрали Function, Bifunction и не продолжили работу до DecaExiFunction?

Ответ на вторую часть - стирание шрифта. После компиляции нет разницы между Func и Func. Поэтому следующее не компилируется:

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

Внутренние функции использовались, чтобы обойти еще одну незначительную проблему. Eclipse настаивал на том, чтобы оба класса в файлах с именем Function находились в одном каталоге ... Не уверен, является ли это проблемой компилятора в настоящее время. Но я не могу отключить ошибку в Eclipse.

Func использовался для предотвращения конфликтов имен с типом функции Java.

Итак, если вы хотите добавить Func от 3 до 16 аргументов, вы можете сделать две вещи.

  • Сделайте TriFunc, TesseraFunc, PendeFunc, ... DecaExiFunc и т. Д.
    • (Я должен использовать греческий или латынь?)
  • Используйте имена пакетов или классов, чтобы имена были разными.

Пример для второго способа:

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

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

и

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

Какой был бы лучший подход?

В приведенных выше примерах я не включил реализации методов andThen () и compose (). Если вы добавляете их, вы должны добавить по 16 перегрузок каждая: TriFunc должен иметь andthen () с 16 аргументами. Это приведет к ошибке компиляции из-за циклических зависимостей. Также у вас не было бы этих перегрузок для Function и BiFunction. Поэтому вы также должны определить Func с одним аргументом и Func с двумя аргументами. В .NET циклические зависимости можно было бы обойти, используя методы расширения, которых нет в Java.

Ганс
источник
2
Зачем вам andThen16 аргументов? Результатом функции в Java является одно значение. andThenпринимает это значение и что-то с ним делает. Также нет проблем с именованием. Имена классов должны быть разными и находиться в разных файлах с одинаковыми именами - в соответствии с логикой, установленной разработчиками языка Java с помощью Function и BiFunction. Кроме того, все эти разные имена необходимы, если типы аргументов разные. Можно создать VargFunction(T, R) { R apply(T.. t) ... }для одного типа.
Alex
2

Я нашел здесь исходный код BiFunction:

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

Я изменил его, чтобы создать TriFunction. Как и BiFunction, он использует andThen (), а не compose (), поэтому для некоторых приложений, требующих compose (), он может не подходить. Это должно быть хорошо для обычных объектов. Хорошую статью об andThen () и compose () можно найти здесь:

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

Вы также можете создать свою собственную функцию, используя 3 параметра

@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
Леандро Маро
источник
1

Вы не всегда можете остановиться на TriFunction. Иногда вам может потребоваться передать вашим функциям n параметров. Затем группе поддержки необходимо будет создать QuadFunction для исправления вашего кода. Долгосрочным решением было бы создать объект с дополнительными параметрами, а затем использовать готовую функцию или биФункцию.

Кушик Рой
источник