Напомню: арность (англ. arity) — это количество параметров функции. Соответсвенно мультиарные функции — это функции с несколькими параметрами. В Java 8 были введены функции с одним и двумя входными параметрами. А как быть если параметров больше?

Когда надо много входных параметров


В Java существует Function<X, R> и BiFunction<X, Y, R>, где X и Y это типы входных параметров, а R — тип выходного параметра. А вот функции с тремя и большим количеством входных параметров необходимо определять самому.
Откуда такая несправедливость? Почему в классе можно определять метод с любым количестаом параметров, а функции с тремя и больше параметров надо определять специально?
Но если надо — попробуем определить. Но как? Наверняка вы слышали о карринге — методе преобразования функции с N параметрами в функцию с N — 1 параметрами. И наверное это первое, что сразу многим прийдет на ум: Мне необходимо мою многопараметрическую (мультиарную) функцию каррировать! Но вот как преобразовать вашу конкретную функцию?
Например, у вас есть функция

$g(x,y) = sin(x + cos(x*y + min(x,y)))$


Вы уже догадались как привести её к виду

$g(x,y) = f2(f1(x), y)$

Или это трудновато? Как он делается, этот самый карринг? Вроде недавно попадалась статья на эту тему…
Не буду вас дальше мучить. Не пугайтесь. Java 8 делает это за вас. Например, чтобы дальше определять функции с тремя входными параметрами:

$r = f(a, b, c)$

вам надо сначала однажды определить интерфейс:
@FunctionalInterface
public interface Function3Arity<A, B, C, R>  {
   R apply(A a, B b, C c);
}

После этого вы можете определять конкретные варианы тернарных (трех-арных) функций. Например вот так:
private static Function3Arity<Integer, String, Integer, String> f3 = 
      (a, op, b) ->{return "" + a + op + b + "=" + (a+b);}; 

Проверим, как это работает:
@Test
public void testFunction3Arity() {
   String result = f3.apply(2, "+", 3);
   assertEquals("2+3=5", result);
}

Соответствующие интерфейсы вы должны определить для каждой используемой арности N=3,4,…
И все бы хорошо, да худо только в том, что прийти к этому решению рациональным путем просто невозможно. (Если Вы как и я не являетесь экспертом в области функционального программирования). Я имею ввиду способность метода apply воспринимать и правильно интерпретировать произвольное количество параметров. В документации это не написано. И не написано, можно ли это сделать как-нибудь другим способом. А я, когда передо мной возникла эта задача, надеялся найти нечто подобное в спецификации класса Function или содержащего её пакета. Например здесь или здесь.

Когда надо много выходных параметров


Мы рассмотрели пример, где было много входных параметров. А что делать, если у нас много выходных параметров?
Как известно, Java позволяет с помощью return возвращать только один примитивный элемент или объект. А хотелось бы иметь возможность уже на уровне сигнатуры функции различать входные и выходные параметры. Т.е. иметь сигнатуры типа:

$(c, d) = f(a, b)$

К сожалению, сделать это напрямую не получится. Выходные параметры надо как-то структурировать. Для этого можно создавать временный обьект либо записывать параметры в список (List<?>). Первыи способ тяжеловесен а второй неприятен потерей статического контроля над типами выходных параметров, если эти типы разные.
С моей точки зрения, более элегантным является использование <TupleN<X1,X2, ..Xn>.
Например, класс Tuple2 выглядит вот так:
public class Tuple2<A, B> {
   public final A a1;
   public final B a2;
   
   public Tuple2(A t, B u) {
       a1 = Objects.requireNonNull(t);
       a2 = Objects.requireNonNull(u);
   }
   
   @Override
   public boolean equals(Object o) {…}
     
   @Override
   public int hashCode() {…}
}

С помощью этого класса функцию с тремя входными и двумя выходными параметрами можно определить вот так:
private static Function3Arity<Integer, String, Integer, Tuple2<Integer, String>> f3And2 =
      (a, op, b) ->{
         int intValue = a + b;
         String sValue = "" + a + op + b + "=" + (a+b);
         return new Tuple2<>(intValue, sValue);
         };     

Проверим как это работает:
@Test
public void testFunction3And2Arity() {
   Tuple2<?,?> result = f3And2.apply(2, "+", 3);
   assertEquals(5, result.a1);
   assertEquals("2+3=5", result.a2);
}

Заключительное правило


  1. Если в вашей функции 3 и больше входных параметра(ов) — вам необходимо определить новый N-арный интерфейс и с его помощью в последующем определять конкретные функции.
  2. Если в вашей функции 2 и больше выходных параметра(ов) — определите класс TupleN и пакуйте в него параметры перед выводом из функции с помощью return.

Код примеров вы найдете в моём проекте на GitHub здесь.

Комментарии (15)


  1. alek_sys
    09.12.2017 22:07

    Я бы предложил пару вещей, которые можно сделать со статьей, чтобы она больше соответствовала формату:


    1. Оформлять блоки кода как блоки кода
    2. Пометить статью как "Tutorial", т.к. это базовые знания по Java и будут полезны начинающим
    3. Лично я бы убрал загадочные формулы с синусами и упоминание карринга, это совершенно нерелевантно
    4. Быть более снисходительным к документации, вещи вроде "И все бы хорошо, да худо только в том, что прийти к этому решению рациональным путем просто невозможно" не совсем отражают положение вещей. Мне кажется, любой, кто использует Java 8 больше чем пару дней знает про функциональные интерфейсы и они описаны в ссылке на документацию, которая приводится в статье — Function прямо говорит, что это Functional interface.


    1. visirok Автор
      09.12.2017 22:47

      Спасибо за Ваши замечания. Постараюсь ответить на некоторые.
      1. Вы можете проверить это в HTML страницы- кодовые блоки оформлены в соответствии с возможностями сайта с помощью HTML-блоков code. Но я согласен, код выглядит плохо. Я заметил, некоторые авторы помещают цветные скриншоты. Согласен, оформлять код надо получше, с цветами элементов и т.д.
      2. Tutorial для меня нечто большее.
      3. Пассаж с каррингом очевидно на Вас не подействовал. Суть вопроса в том, как нормальный Java программист, не знающий про магию функции apply будет определять функцию с тремя аргументами. Наверное он вспомнит что есть карринг. Шутка не удалась.
      4. Про снисходительность совершенно с Вами не согласен. Речь идет о том, что функция apply не-до-документирована. Её важнейшее свойство не описано вообще. Однако — спасибо за Ваше замечание. Я подправил текст и усилил именно этот аспект.


      1. visirok Автор
        09.12.2017 23:44

        Или мне показалось, или на сервере Хабра работает постпроцессор. Теперь я уже не очень уверен, но мне кажется, первый час после опубликования цветов в фрагментах кода не было. Соответствующих форматирований нет и в моём оригинальном коде. Это точно.
        Видимо этим объясняется и предложение N1 из певого комментаоия.
        Видимо постпроцессор за этот час поработал и сделал код симпатичнее.


        1. visirok Автор
          10.12.2017 12:32

          Моя гипотеза с постпроцессором подтвердилась. Только в качестве «постпроцессора» выступил один из редакторов сайта, который любезно заменил HTML тэги предлагаемые редактором текстов на другие, мне до этого не известные.
          Как следствие этой замены, дискуссия о форматирования кода и предложение из первого комментария становятся неактуальными.


      1. alek_sys
        10.12.2017 00:20
        +2

        Функция apply тут не при чем, метод может называться как угодно. Любой интерфейс с единственным методов (SAM, single abstract method) может быть функциональным интерфейсом. И документация про это говорит: "Each functional interface has a single abstract method, called the functional method for that functional interface, to which the lambda expression's parameter and return types are matched or adapted".


        1. visirok Автор
          10.12.2017 00:41

          И все же я писал статью о мультиарных функциях. Как их определять, документация умалчивает. На то я и сетую. Поэтому я метод apply отношу к категории не-до-определённых. Речь идет не об интерфейсах, а о методе apply.


          1. fogone
            10.12.2017 01:36

            Метод apply мог бы назваться горшком и это ничего ты не поменяло. Именно из за этого документация умалчивает об этой «магии». Просто никакой магии тут нет. Ещё раз: не надо назвать метод apply, чтобы интерфейс стал функциональным. Даже аннотация не нужна. Т.е. статья вцелом совершенно непонятно о чем.


            1. visirok Автор
              10.12.2017 12:44

              Эта статья о мультиарных функциях в Java, о чем и говорит её заголовок. Тема эта не очень простая. Об этом можно судить по запросам в разных форумах и иногда очень неожиданным ответам на них. С моей точки зрения, эта тема заслуживает одной странички в любой книге по Java 8 или Java 9. Но мне пока такая книга пока не встретилась. Я не исключаю, что такая книга всё же на свете есть.
              Не оставляю надежды, что некоторым читателям статья показалась полезной в их практической деятельности.


              1. fogone
                10.12.2017 13:32

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


  1. ibessonov
    09.12.2017 22:18
    +2

    Есть у меня несколько замечаний.


    Наверняка вы слышали о карринге — методе преобразования функции с N параметрами в функции с N + 1 параметрами

    Кажется вы не понимаете, что такое "карринг". В Java он выглядел бы, например, как представление объекта BiFunction<X, Y, R> в виде Function<X, Function<Y, R>>. Такой финт позволяет реализовать любую арность только через интерфейс Function.
    Ну а ваш пример с f2(f1(x), y) неверен. Да и дальше по тексту карринг нигде не упоминается.


    прийти к этому решению рациональным путем просто невозможно

    Не соглашусь. Выражать функции 3-х аргументов с помощью методов с 3-мя параметрами это наиболее рациональная и очевидная вещь, какую только можно сделать. Документация подобные факты упоминать не будет по тем же причинам — это и так должно быть очевидно.


    трехарных

    Тернарных.


    1. visirok Автор
      09.12.2017 22:58

      Большой разницы между вашим примером для BiFunction… и моим для f2 я не вижу, только порядок аргументов разный.
      Про очеаидность не соглашусь. Попробуйте сделать в Stackoverflow запрос «Trifunction java» и посмотрите заодно какие там даются экспертами советы.
      «Трех-арных» было употреблено как бы в созвучие слову из заголовка «мультиарные». Но текст я подправил. Спасибо.


      1. ibessonov
        10.12.2017 14:14

        Большой разницы между вашим примером для BiFunction… и моим для f2 я не вижу

        Возможно я выразился немного запутано, позвольте объяснить подробнее.
        Функция f2 в вашем примере является двуместной, об этом говорит запятая в списке её аргументов. Будь она каррированной, у неё был бы всего один параметр.


        Более формально — пусть у вас есть функция f : (A x B) -> R (где A x B — декартово произведение).
        Каррированная её версия являлась бы отображением g : A -> (B -> R), то есть функцией одного аргумента, результатом работы которой тоже является функция одного аргумента. Такого вида функции как раз часто встречаются в функциональных языках.


        1. visirok Автор
          11.12.2017 12:06

          С примером функции f2 вы правы. Этот литературный прием наверное в данном контексте неуместен, может запутать новичка в функциональном программировании на Java и раздосадовать специалиста. Идея состояла в том, что не надо каррировать функции самому вообще.
          Чтобы Ваш комментарий остался понятен и последующим читателям, я тем не менее не буду в этом месте изменять текст статьи.


  1. LorDCA
    10.12.2017 12:51
    -1

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


    1. visirok Автор
      10.12.2017 12:57

      Фактически вы ставите под сомнение необходимость использования функций в Java. Зачем нужны функции, если есть объекты?
      Но это самостоятельный вопрос, ответ на который разработчики Java дали в верси 8 введением функций в состав языка.
      А моя статья рассказывает о специальной разновидности функций.
      Кстати, метод использования многих выходных параметров актуален и для обьектов.