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 запитає, чому б не чотирифункціональність, а по-друге, вам потрібно дублювати всі методи, які приймають Bifunction як параметр
user902383

6
Існує точка зменшення віддачі для таких API. (Особисто я думаю, що JDK8 передав це деякий час назад, але це навіть більше того.)
Луї Вассерман

Я вважаю, що обґрунтуванням було сказати, що Function та BiFunction були повністю реалізовані з об'єктами та нативними типами. Включення TriFunctions з усіма різними варіантами підірве JRE класами та методами.
Thorbjørn Ravn Andersen

1
Коротка відповідь. У Java, якщо ви цього не бачите, ви створюєте свій власний (див. Відповіді Алекса Р, звичайно). Sidenote, в C #, реалізатори dotnet дали вам попередньо консервовані (до 16 аргументів), але без імен префіксу ("Bi" тут): див. Docs.microsoft.com/en-us/dotnet/api/… Просто простий "Функ". Тож це одне з місць, де я віддаю перевагу 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взагалі існує. Що не означає, що його слід видалити! Я борюся за своїх монадів, поки не помру!

Отже, якщо у вас є багато аргументів, які ви не можете об'єднати в логічний клас контейнерів, і якщо вам потрібна функція, щоб бути конструктивною, використовуйте посилання на метод. Інакше спробуйте використовувати нову здобуту здатність деструктивних функцій, ви можете виявити, що ви робите багато справ із набагато меншими рядками коду.


2
Ви відповіли на моє запитання ... Я думаю ... Я не знаю, чи дизайнери мови Java походять з цього напряму мислення, але я не добре розбираюся у функціональному програмуванні. Дякую за пояснення.
Річард Фінеган

79
Я ніколи не бачив термінів конструктивних і руйнівних, що використовуються для позначення описаних вами понять. Я думаю, що вибагливі та нежиті - це більш поширені терміни.
Feuermurmel

17
Перший приклад функції не синтаксично правильний. Це має бути BiFunction, а не Function, оскільки він займає два вхідні аргументи.
оголосити

3
IMO BiFunctionстворений для легкого скорочення даних, і більшість Streamоперацій з терміналом - це лише скорочення даних. Хороший приклад BinaryOperator<T>, який використовується у багатьох Collectors. Перший елемент зменшується з другим, ніж потім може бути зменшений з наступним тощо. Звичайно, ви можете створити Function<T, Function<T, T>func = x -> (y -> / * код скорочення тут * /). Але, серйозно? Все це, коли можна просто зробити BinaryOperator<T> func = (x, y) -> /*reduction code here*/. Плюс цей підхід до зменшення даних здається мені схожим на ваш "руйнівний" підхід до мене.
ФББ

32
Як це отримало стільки результатів? Це жахлива і заплутана відповідь, оскільки вона заснована на припущенні, що Function<Integer,Integer> f = (x,y) -> x + yє дійсним Java, а це не так. Це має бути 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

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


6
Дякую за рішення. І так, безумовно, є використання для BiFunction, TriFunction, ... Інакше люди не шукали б цього. Здається, вся лямбда-річ просто занадто нова для Oracle зараз і буде розширена в наступних версіях Java. На даний момент це більше доказ концепції.
Стефан Ендруліс,

Hy @ Алекс, чи можете ви, будь ласка, визначити наступний рядок. що відбувається тут за замовчуванням <V> TriFunction <A, B, C, V> andThen (Функція <? super R,? розширюється V> після) {Objects.requireNonNull (після); повернути (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()приклад використання.
Алекс Пакка

Не тільки currying не дуже добре адаптований до мови Java, але також, виправте мене, якщо я помиляюся, але BiFunctionвикористовується в StreamAPI для зменшення даних, що дуже схоже на підхід до мене: ви ніколи не приймаєте більше двох аргументи, і ви можете обробити будь-яку кількість елементів, одне скорочення за раз (див. мій коментар до прийнятої відповіді. Я був би радий знати, якщо я помиляюсь, коли це бачу).
ФББ

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
Я збирався оновити свою відповідь, щоб згадати вавр, але ви були першими, тому я схвалив. Якщо ви доберетеся до потрібної TriFunction, є велика ймовірність, що вам буде краще, використовуючи vavrбібліотеку - це робить програмування функціонального стилю максимально переносимим на Java.
Олексій

7

У мене майже те саме питання і часткова відповідь. Не впевнений, чи конструктивна / деконструктивна відповідь - це те, що мали на увазі мовні дизайнери. Я думаю, що в 3 і більше up N є справжні випадки використання.

Я родом з .NET. і в .NET у вас є Func і Action для недійсних функцій. Присудок та деякі інші особливі випадки також існують. Дивіться: https://msdn.microsoft.com/en-us/library/bb534960(v=vs.110).aspx

Цікаво, в чому причина, чому дизайнери мови вибрали функцію, біфункцію і не продовжували роботу до 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 наполягав на тому, щоб обидва класи у файлах з назвою Функція знаходилися в одному каталозі ... Не впевнений, чи це проблема компілятора в даний час. Але я не можу повернути помилку 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 повинен мати athen () з 16 аргументами. Це призведе до помилки компіляції через кругові залежності. Крім того, у вас не було б цих перевантажень для функцій та BiFunction. Тому слід також визначити функцію з одним аргументом і функцію з двома аргументами. У .NET кругові залежності можна було б обійти за допомогою методів розширення, яких немає в Java.


2
Навіщо тобі потрібно andThen16 аргументів? Результатом функції в Java є єдине значення. andThenприймає цю цінність і щось робить з нею. Крім того, з іменами не виникає проблем. Назви класів повинні бути різними та бути у різних файлах, названих однаковими - дотримуючись логіки, встановленої розробниками мови Java з функціями та BiFunction. Також всі ці різні імена потрібні, якщо типи аргументів різні. Можна створити VargFunction(T, R) { R apply(T.. t) ... }для одного типу.
Олексій Пакка

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

0

Ви не завжди можете зупинитися на TriFunction. Іноді вам може знадобитися передати n кількість параметрів вашим функціям. Тоді команді підтримки доведеться створити QuadFunction, щоб виправити ваш код. Довгостроковим рішенням буде створити Об'єкт із додатковими параметрами, а потім використовувати готову функцію або BiFunction.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.