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

  • Расширение набора интерфейсов

  • Карринг и частичное применение

  • Перехват

  • Обработка исключений

  • Мультиметоды

Проект на GitHUB

multifunctions

Maven

multifunction

Библиотека написана на Java 21 и не содержит зависимостей.

<dependency>
    <groupId>com.aegisql.multifunction</groupId>
    <artifactId>multifunction</artifactId>
    <version>1.1</version>
</dependency>

Расширение набора интерфейсов

В Java стандартно поддерживаются функциональные интерфейсы с не более чем двумя аргументами. Это такие интерфейсы как BiFunction, BiConsumer, BinaryOperator и некоторые другие. Практически полезно иметь возможность неограниченно увеличивать количество аргументов по мере необходимости, а так же избегать ненужных повторов. Кроме того, все расширения интерфейсов реализованы единообразно и имеют взаимосвязь друг с другом.

Об именах интерфейсов

Два основных типа интерфейсов - функции и консьюмеры. Функция возвращает результат. Консьюмер - нет (тип void).
Соответственно, все интерфейсы для функций называются Function1, Function2, … FunctionN по количеству своих аргументов. Аналогично для консьюмеров: Consumer1, Consumer2ConsumerN.
Функция с нулем аргументов представлена интерфейсом SupplierExt.
Консьюмер с нулем аргументов - интерфейсом RunnableExt.
Также, определены интерфейсы Predicate[N] и ToInt[N]Function, как аналоги Predicate и ToIntFunction из стандартной коллекции интерфейсов. В настоящее время они имеют вспомогательную роль и не получили, должного развития. Это может измениться в будущем.
В первом релизе максимальное количество аргументов равно десяти. Это интерфейсы Function10 и Consumer10. Число аргументов может быть увеличено до любого практически необходимого значения с помощью утилит - генераторов кода.

Инстациация интерфейсов.

Как и все прочие функциональные интерфейсы, путем объявления переменной функционального типа с указанием типа каждого из аргументов.

Function2<Double,Double,Double> min2 = Math::min;
Function3<Double,Double,Double,Double> min3 = (x,y,z)->Math.min(x,Math.min(y,z));

Даже на примере короткой трехпараметрической функции видно, что сложность определения быстро увеличивается. Для данного примера, к сожалению, альтернативы нет, поскольку модуль Math содержит разные версии функции min() для разных типов, и компилятору требуется явная подсказка. Если же неоднозначность отсутствует, в каждом интерфейсе существует статический метод of(…), позволяющий существенно упростить ввод.

public static String hello(String greet, String user) {
    return greet+", "+user+"!";
}
…
var greet = Function2.of(Greetings::hello);

Так для любого количества аргументов.

Карринг и частичное применение

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

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

Карринг дает следующие преимущества:

  • Более простой интерфейс с меньшим количеством параметров.

  • Фиксация некоторого количества аргументов позволяет получить функцию зависящую только от изменчивых аргументов, что снижает вероятность ее неправильного применения.

  • Частичное применение позволяет из одной функции получить целое семейство функций с более четко определенным назначением.

  • Ленивое выполнение. Аргументы могут быть частично применены там и тогда где они доступны. Окончательное же связывание всех аргументов происходит только в момент выполнения конечной функции с оставшимися аргументами.

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

Каждый интерфейс N-го порядка имеет N пар методов applyArg[i] для функции и acceptArg[i] для консьюмеров. Пар, поскольку один из методов принимает значение, а второй Supplier для значения. Метод возвращает Функцию или Консьюмер порядка N-1.

Примеры:
Пусть у нас есть метод генерирующий приветствие для обращения к клиенту:

public String hello3(String greet, String pref, String user) {
    return greet+" "+pref+" "+user+"!";
}

В соответствии с полом клиента префикс может быть Mr. Или Mrs.
Метод hello3 может быть преобразован в семейство функций, каждая со своим значением префикса. Заметим, что префикс - это второй аргумент метода, значит, для его частичного применения понадобится метод applyArg2(String arg):

var greet3 = Function3.of(this::hello3);
var maleGreet = greet3.applyArg2("Mr.");
var femaleGreet = greet3.applyArg2("Mrs.");

maleGreet и femaleGreet имеют тип Function2 , то есть, от трех аргументов осталось два. greet и user.

String smithGreet = maleGreet.apply("To:", “Smith"); //To: Mr Smith!
String leeGreet = femaleGreet.apply("Dear", “Lee"); //Dear Mrs. Lee!

Можно продолжить и получить функцию требующую только имя пользователя.

var toMaleGreet = maleGreet.applyArg1("To:"); //For formal letters
String smithGreet = toMaleGreet.apply("Smith");

Можно захватить сразу все параметры, получив беспараметрический SupplierExt.

SupplierExt<String> helloWorldGreet = greetр.lazyApply(“Hello”,”Brave New", “World"); 
var helloWorld = helloWorldGreet.get();//Hello Brave New World!

Анкарринг

Если функция получена путем картинга другой функции, то ее можно “распаковать”, получив обратно исходную функцию.

Function3<Object, String, String, String> uncurry = maleGreet.uncurry();

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

Function3<String, String, String, String> uncurry = maleGreet.uncurry();

Если анкарринг не желателен, просто оберните функцию методом of() соответствующего интерфейса. Попытка анкарринга вызовет UnsupportedOperationException.

var mrGreet = Function2.of(greet3.applyArg2("Mr."));
mrGreet.uncurry(); // throws exception

Перехват

Иногда требуется выполнить какие-то действия до (метод before или после (метод after) основной функции. Например, сделать валидацию параметров, поместить запись в лог, послать уведомление о вызове функции. Для консьюмеров контекст до и после полностью определяется аргументами. Поэтому, в качестве перехватчиков используются дополнительные консьюмеры той же размеренности и того же типа аргументов, что и основной консьюмер.
Для функций before вызова контекст так же определяется только аргументами, но после вызова финки в контексте возникает новый элемент - результат, который должен учитываться в постфиксном консьюмере метода after. Его размеренность на единицу больше, и включает тип результата. Так же как и в случае анкарринга перехват результата не поддерживается для крайних в имплементации интерфейсах.
Также, все функции поддерживают метод andThen, знакомый по стандартным интерфейсам, позволяющий трансформировать результат к другому значению или типу.

Пример:
Мы хотим посчитать и отформатировать проценты используя следующий метод:

public static double div(double x, double y) {
    return x/y;
}

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

var div = Function2.of(RandomTest::div);
var intercepted = div.before((x, y) -> {
            if (x < 0 || y < 0) throw new RuntimeException("arguments must be positive");
            if (y == 0) throw new RuntimeException("y cannot be zero");
        })
        .after((x, y, res) -> {
            System.out.println(x + "/" + y + "=" + res);
Ss        })
        .andThen(res->"%.1f%%".formatted(100*res));

intercepted.apply(1d,-10d); //This will throw an exception

String res = intercepted.apply(11.0, 37.0); 
//29.7%

Обработка исключений

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

Вопрос на который мы должны ответить, когда сталкиваемся с необходимостью обработки исключений, какой эффект мы хотим произвести? Вариантов, на самом деле, не так много. Мы можем:

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

  • Обернуть перехваченное исключение в RuntimeException и бросить его.

  • Вернуть дефолтное значение, включая null.

  • Вернуть Optional.empty()

  • Произвести какие то дополнительные действия, после чего выполнить что-то из выше перечисленного.

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

@FunctionalInterface
public interface Function3 <A1,A2,A3,R> {
   @FunctionalInterface
    interface Throwing<A1,A2,A3,R>{ R apply(A1 a1,A2 a2,A3 a3) throws Exception; }
…

Методы throwing

Семейство статических методов throwing(…) работаот аналочично методу of(…),
Но в отличие от него могут принимать функции бросающие исключения, и возвращают функцию обернутую в try-catch блок. throwing можно применять и к тем функциям, которые бросают RuntimeException, если желательно его перехватить.

Следующий пример я хотел бы представить в виде законченного мини-проекта.
Допустим, мы хотим подсчитывать размер какой либо директории. Без или с заходом вглубь поддиректорий.
Для этого можно использовать стандартный метод Files.walkFilesTree(…) JavaDoc

Который определен так:

public static Path walkFileTree(Path start,
                                Set<FileVisitOption> options,
                                int maxDepth,
                                FileVisitor<? super Path> visitor)
                         throws IOException

Выглядит мудрено.
Первое, что мы видим, это то, что метод принимает 4 параметра, возвращает Path, и кидает IOException. Начнем с очевидного - обернем метод в экземпляр Function4 использованием метода throwing.

public static final Function4<Path, Set<FileVisitOption>, Integer, FileVisitor<Path>, Path> FILE_TREE_WALKER = Function4.throwing(Files::walkFileTree);

Данная функция пока не сильно облегчила нашу задачу, разве что, мы избавились от исключения. При желании, можно было бы применить другие варианты метода throwing(…). С их помощью можно, например, изменить формат сообщения об ошибке, заменить дефолтный RuntimeException на любой другой, производный от него, либо полностью перехватить обработку ошибки вместе со всем контекстом вызова функции.

Смотрим дальше. enum FileVisitOption предлагает только одну опцию - следовать символичным линкам. Соответствующий сет может быть либо пустым (не следовать), либо содержать FOLLOW_LINKS (следовать). Отлично! Преобразуем дальше нашу функцию в две с меньшей размеренностью.

public static final Function3<Path, Integer, FileVisitor<Path>, Path> FOLLOW_LINKS_FILE_TREE_WALKER = FILE_TREE_WALKER.applyArg2(Set.of(FOLLOW_LINKS));
public static final Function3<Path, Integer, FileVisitor<Path>, Path> NO_FOLLOW_LINKS_FILE_TREE_WALKER = FILE_TREE_WALKER.applyArg2(Collections::emptySet);

Дальше. maxDepth задает глубину рекурсии обхода дерева директорий. Здравый смысл подсказывает, что в 90% случаев нас будет интересовать либо размер директории первого уровня, либо полный размер, включающий все под-директории. Прекрасно!. Избавимся еще от одной размеренности.

public static final Function2<Path, FileVisitor<Path>, Path> DEEP_FOLLOW_LINKS_FILE_TREE_WALKER = FOLLOW_LINKS_FILE_TREE_WALKER.applyArg2(Integer.MAX_VALUE);
public static final Function2<Path, FileVisitor<Path>, Path> SHALLOW_FILE_TREE_WALKER = NO_FOLLOW_LINKS_FILE_TREE_WALKER.applyArg2(1);
public static final Function2<Path, FileVisitor<Path>, Path> DEEP_NO_FOLLOW_LINKS_FILE_TREE_WALKER = NO_FOLLOW_LINKS_FILE_TREE_WALKER.applyArg2(Integer.MAX_VALUE);

Давайте теперь разберемся с визитором. Тут без вариантов. Придется создать визитор для подсчета размера директории. Для простоты и экономии места предложим следующий вариант.

public static class SizeCountingFileVisitor extends SimpleFileVisitor<Path> {
    private final AtomicLong counter = new AtomicLong();
    public long getCollectedSize() {return counter.get();}
    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
        counter.addAndGet(attrs.size());
        return CONTINUE;
    }
}

В следующем разделе мы посмотрим, а какие вообще альтернативы визиторам возможны, а пока - данный класс просто содержит атомарный счетчик, который увеличивает размер на размер очередного обнаруженного файла. Ну и, интерфейс доступа к накопленному значению.

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

String path -> long size

Преобразование строки в Path это, по сути, Function1

public static final Function1<String, Path> PATH_OF = Function1.of(Path::of);

И остался последний шаг. Собираем все вместе. Например, так.

public static final SupplierExt<Function1<String,Long>> NEW_COUNTING_SHALLOW_FILE_TREE_WALKER = ()->{
    var visitor = new SizeCountingFileVisitor();
    var sizeCountingWalker = SHALLOW_FILE_TREE_WALKER.applyArg2(visitor);
    var walker = PATH_OF.andThen(sizeCountingWalker);
    return walker.andThen(_ -> visitor.getCollectedSize());
};

public static final SupplierExt<Function1<String,Long>> NEW_COUNTING_DEEP__FILE_TREE_WALKER = ()->{
    var visitor = new SizeCountingFileVisitor();
    var sizeCountingWalker = DEEP_NO_FOLLOW_LINKS_FILE_TREE_WALKER.applyArg2(visitor);
    var walker = PATH_OF.andThen(sizeCountingWalker);
    return walker.andThen(_ -> visitor.getCollectedSize());
};

Используем.

var shallowWalker = NEW_COUNTING_SHALLOW_FILE_TREE_WALKER.get();
System.out.println("Shallow Size: "+shallowWalker.apply("./src/main/java/com/aegisql/multifunction/"));

var deepWalker = NEW_COUNTING_DEEP_FILE_TREE_WALKER.get();
System.out.println("Deep Size: "+deepWalker.apply("./src/main/java/com/aegisql/multifunction/"));

Вывод:

Shallow Size: 333399
Deep Size: 351563

Итак, мы получили целое семейство полезных и безопасных функций не создав ни одного лишнего класса. (Кроме визитора, которого требовал интерфейс)

Методы optional и orElse

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

Рассмотрим для примера функцию деления:

Function2<Integer,Integer,Integer> div = (x,y)->x/y;
Integer res = div.apply(10, 0);
//java.lang.ArithmeticException: / by zero

Деление на ноль вызвало исключение.

Function2<Integer, Integer, Optional<Integer>> divOptional = div.optional();
Optional<Integer> resOptional = divOptional.apply(10, 0);

Безопасная операция. Функция вернула Optional.empty()

Если преобразование возвращаемого типа в Optional не желательно, можно воспользоваться дефолтным значением.

Function2<Integer, Integer, Integer> divDef = div.orElse(() -> null); 

Integer resDef = divDef.apply(10, 0);
//NULL

В данном случае в качестве дефолтного значения мы выбрали null, который никак не может получиться в результате обычного деления. Использование “магических значений” должно применяться с осторожностью.

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

Мультиметоды

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

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

В библиотеке представлено два сорта статических методов dispatch(…) В интерфейсах Function[N] диспетчеры возвращают Function[N], “обертывающую” логику работы диспетчера и вызываемых функций в единую функцию. Соответственно, в интерфейсах Consumer[N] возвращается Consumer[N]

Первый метод dispatch принимает Predicate[N] в качестве первого аргумента, и две Function[N]. Первая функция вызывается если предикат вернул истину. Второй - если ложь.

Второй метод dispatch принимает ToInt[N]Function в качестве первого аргумента, и массив переменной длины функций Function[N]. ToInt[N]Function должна вернуть в качестве результата индекс нужной функции в передаваемом массиве.

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

Function3<String,Double,Double,Double> dispatch = Function3.dispatch(
        (op,_,_)->switch (op.toUpperCase()) {
            case "MIN" -> 0;
            case "MAX" -> 1;
            case "POW" -> 2;
            case "ROOT_N" -> 3;
            case "MUL" -> 4;
            case "DIV" -> 5;
            default -> throw new UnsupportedOperationException(STR."Unsupported operation: \{op}");
        },
        (_,x,y)->Math.min(x,y),
        (_,x,y)->Math.max(x,y),
        (_,x,y)->Math.pow(x,y),
        (_,x,y)->Math.pow(x,1/y),
        (_,x,y)->x*y,
        (_,x,y)->x/y
);

Double min = dispatch.apply("MIN", 10d, 15d);
//10
Double max = dispatch.apply("MAX", 10d, 15d);
//15
Double pow = dispatch.apply("POW", 2d, 3d);
//8
Double root = dispatch.apply("ROOT_N", 8d, 3d);
//2
Double mul = dispatch.apply("MUL", 2d, 3d);
//6
Double div = dispatch.apply("DIV", 2d, 3d);
//0.66666667
dispatch.apply(“LOG_N",2d,1024d);
// throws “UnsupportedOperationException”               

Заключение

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

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


  1. Sigest
    06.05.2024 04:16
    +1

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


    1. TerraV
      06.05.2024 04:16
      +1

      Я бы эту библиотеку на ревью просто не пропустил. Причина здесь несколько:

      • Плохая читаемость кода.

      • Инструмент поставлен вперед задачи. Задачи из примеров прекрасно решаются прощее/понятнее с использованием стандартных инструментов Java. А на Kotlin это была бы вообще песня.

      • Хорошее API удобно использовать, лучшие API сложно использовать неправильно. Из того что я увидел, можно ожидать очень странных конструкций, особенно от разработчиков уровня Junior/Middle.

      P.S. Часть функций вообще провоцируют антипаттерны. В частности dispatch из примера это конкретный код за который хочется очень обстоятельно пояснить почему это плохо.


    1. aegisql Автор
      06.05.2024 04:16

      Уменьшить страдание я и хотел. Тема о том, "а вот в Хаскеле это намного круче" конечно интересная, но я собственно адресую к тем, кому не шашечки, а ехать. Java так Java.


    1. sshikov
      06.05.2024 04:16

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


      1. aegisql Автор
        06.05.2024 04:16

        О! Спасибо что напомнили о стримах.

        Сколько раз вы сталкивались с необходимостью получить индекс объекта в стриме?

        Вот как это можно сделать с помощью карринга и SupplierExt

                List.of(10,20,30,40,50,60,70,80,90,100)
                        .stream().forEach(Consumer2.of((AtomicInteger i,Integer x)->{
                            System.out.println(i.get()+"*"+x+"="+(i.getAndIncrement()*x));
                        }).acceptArg1(SupplierExt.ofConst(new AtomicInteger())));
            }

        Заметьте, AtomicInteger нигде вне контекста не доступен, и никто не сможет как то исказить данные.


        1. TerraV
          06.05.2024 04:16
          +2

          Вы прям реально считаете что написали нормальный код? Вот правду говорят у человека с молотком все проблемы выглядят гвоздями.

          Аналогичный код в Java парадигме
                  List<Integer> numbers = Arrays.asList(12, 23, 37, 45, 58, 64, 79, 83, 91, 102); // Произвольный список чисел
          
                  IntStream.range(0, numbers.size()) // Создаем поток индексов от 0 до размера списка
                          .forEach(i -> System.out.println(i + "*" + numbers.get(i) + "=" + (i * numbers.get(i))));

          Если че это код сгенерировал ChatGPT. На порядок легче в поддержке.

          Оптимизация кода (от ChatGPT)

          1. Использование стандартных интерфейсов: Замените Consumer2 и SupplierExt на стандартные интерфейсы Java для упрощения и улучшения поддержки кода.

          2. Избегание глобального состояния: Код использует AtomicInteger для учета индекса внутри лямбда-выражения, что не является идеальным для функционального стиля, где предпочтительнее избегать изменяемого состояния. Вместо этого мы можем использовать метод IntStream для генерации индексов.

          3. Эффективное использование Stream API: Метод forEach предназначен для операций без возврата значения, но в вашем случае выглядит, что вы хотите использовать значения индексов, что лучше делать через метод map.

          Вот ровно про это я и писал что эта библиотека провоцирует написание плохого кода


          1. aegisql Автор
            06.05.2024 04:16

            Ну а вы, видимо, считаете, что раскрыли мне глаза.

            Ну модифицируйте теперь этот код для двух индексов, трех, четырех.

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

            Мои расширения, конечно, не дают почти никаких преимуществ на числе параметров 1 и 2, которые уже реализованы стандартно, а у меня нужны исключительно для связности.


            1. TerraV
              06.05.2024 04:16
              +1

              Я не пишу код ради кода. Я пишу код под задачу. Есть желание потягаться - я не против. Ставьте задачу и посмотрим у кого код лучше.


        1. sshikov
          06.05.2024 04:16

          Ну, вообще говоря, в стриме нет естественного индекса объекта. Если вам хочется - сделайте стрим из пар (индекс, объект), либо как вы поступили, либо zipWithIndex. И имейте при этом в виду, что как только вы начнете обработку стрима параллелить, ваш атомик возможно станет бутылочным горлышком :)


          1. aegisql Автор
            06.05.2024 04:16

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


            1. sshikov
              06.05.2024 04:16

              Против идеи ничего не имею, идея норм. Я про то, что стримы (в отличие от циклов) хорошо параллелятся, но не всегда, а скажем если в них нет вот таких вот операций.


        1. Stingray42
          06.05.2024 04:16

          Когда мне нужно получить индекс объекта в стриме, я обычно использую старый добрый for. Работает безотказно.


  1. GeniyZ
    06.05.2024 04:16

    Присоединяюсь к первому комментарию.
    Благо есть Котлин, — если есть возможность, переходите на него)


    1. aegisql Автор
      06.05.2024 04:16
      +1

      То что в Котлин ФП реализовано получше я не спорю. Было бы и странно, если не так. Но, в защиту Java, ФП в нее было добавлено поверх грандиозной легаси. Добавлено с полным сохранением обратной совместимости кода. А это подвиг почти.

      Вспомните, любители Скалы, танцы с бубнами вокруг перехода на версию 2.1


  1. panzerfaust
    06.05.2024 04:16
    +2

    Честно, я прочитал уже пару десятков статей про ФП, и везде каррирование сопровождается каким-то патологически плохим примером. Сначала идут дифирамбы этому приему, а потом берут какую-то нормальную функцию и превращают ее в ФП-абракадабру. Я решительно не понимаю ни где это может помочь мне в моей рутине, ни зачем вообще этим заниматься. Про монады, функторы, функции высшего порядка - все кристально ясно (и они реально популярны на практике), а вот с каррированием какая-то шляпа.


    1. sshikov
      06.05.2024 04:16

      зачем вообще этим заниматься

      Затем что это упрощает некоторые вещи с точки зрения функций высших порядков. Потому что иметь везде унарные функции местами удобнее. Ну например map - вас же не смущает, что у map в сигнатуре унарная функция, вместо многих разных map с функциями разной -арности?

      Про пример ничего сказать не хочу, вполне допускаю, что игрушечный пример и правда плох. И я готов согласиться, что придумать хороший пример и правда сложно, и что каррирование это не то, что применяется на повседневной основе - все скорее правда.

      Но неясного-то в нем что?


  1. maxzh83
    06.05.2024 04:16
    +1

    Идите знаете куда со своим каррированием? В Scala идите) А если серьезно, то статья интересная для расширения кругозора, но надо писать, что все выполнено профессионалами и повторять такое в проде дома не надо.


    1. aegisql Автор
      06.05.2024 04:16

      С каррированием лучше в Кложур, если уж на то. ;)


      1. maxzh83
        06.05.2024 04:16

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


        1. aegisql Автор
          06.05.2024 04:16

          Скала пытается усидеть сразу на всем. И в этом ее беда. Кложур же является диалектом Лиспа, и карриривание просто естественная часть его жизненного цикла разработки. У него другая проблема, как и у любого Лиспа. Недостаток выразительных средств. Все терема высекаются одним топором.

          Вообще "зачем все это нужно" вопрос правильный, но заслуживающий не стали даже, а книги целой. И, сдается мне, они уже написаны. В двух словах, последовательное применение FP сильно облегчает написание Data Oriented программ. То есть, таких программ, где из за сложности данных, с которыми приходится работать, сама по себе парадигма ООП становится антипатерном.

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

          И тут полезно иметь какой то шаблон, а не переизобретать велосипед каждый раз.


          1. maxzh83
            06.05.2024 04:16
            +1

            Скала пытается усидеть сразу на всем. И в этом ее беда

            Возможно, но это не относится к теме. Конкретно под вашу задачу Scala подходит замечательно, она такой задумывалась и проектировалась. Если она вам не нравится по каким-то причинам, то это ваш выбор.

            а не переизобретать велосипед каждый раз

            Кажется, что ваше решение как раз напоминает велосипед


            1. aegisql Автор
              06.05.2024 04:16

              К теме как раз не относится ни Скала, ни Котлин, ни F#. Потому что есть корпоративная реальность. Вы приходите на проект и вам сообщают, что писать надо на Java. Ваше блеяние о том, что Котлин лучше никого из менеджмента не интересует. Соответственно, цель всего этого - выжать максимум из того что доступно в данном конкретном инструменте.


              1. maxzh83
                06.05.2024 04:16
                +1

                Вернитесь к первоначальному комментарию, про Скалу это шутка была. А что не шутка, так это то, что не надо такое тащить в прод, если это не стартап или ваш пет-проект. На джаве пишут обычно энтерпрайз разной степени кровавости. И в нем очень важна читабельность и поддерживаемость кода, часто важнее производительности и лаконичности. А такие "выжимания максимума" это как раз то, что делает энтерпрайз кровавым. Вы поразвлекаетесь и уйдете в другое место писать на Clojure, а кому-то разгребать потом ваши каррирования. Как обучающе-развлекательная статья в стиле "видали чё можно на вашей Джаве" - норм.


                1. aegisql Автор
                  06.05.2024 04:16

                  А чем, мне правда интересно, читабельность хуже? Она просто такая какая есть. Нут ничего изменить нельзя. Если и функции 10 параметров, то в Java вынь да положь перечислить их все. включая подтипы.

                  И что непонятного в, например, applyArg1(value)?

                  Функция вернула функцию. Обычный прием. Только на кор джава в разы больше набирать. Вот уж где точно с читабельностью полный затык будет.

                  Если у человека достаточный опыт чтения джавовского функционального кода, то лично я не вижу никакой разницы.


                  1. maxzh83
                    06.05.2024 04:16
                    +1

                    Если и функции 10 параметров, то в Java вынь да положь перечислить их все. включая подтипы.

                    Никто не мешает отрефакторить и сделать, чтобы функция принимала меньше параметров. А параметры сделать полями объекта с builder. Это делается одной аннотацией Lombok. И тогда вместо applyArg1(value) будет нормальное имя поля и также передавать можно не все поля. Если такая функция из библиотеки, то можно написать к ней свой адаптер, как вариант. Если все параметры не нужны, можно сделать перегруженный метод и т.д.

                    При этом любой джава-разработчик такое поймет и ему не надо иметь "достаточный опыт чтения джавовского функционального кода".


                    1. aegisql Автор
                      06.05.2024 04:16

                      Два момента:

                      1) сборка мусора. Параметры функции передаются через стек, поля объекта создаются в куче. Начните только профайлинг. Много интересного узнаете.

                      2) Ад бесконтрольного роста количества классов с единственной целью, удовлетворить какую то одну функцию. От функции то вы ведь все равно не избавитесь. Так как она работу делает. А лишний класс - это зло.

                      3) По хорошему, а нахрена теперь Ломбок, если есть рекорды? У меня мало в каких проектах применяется. Ей Богу, когда меня начинают убеждать, что сроки сорваны из за набора сеттеров и геттеров, которые все равнo IDE сгенерировала, то не знаешь, то ли смеяться, то ли по ушам настучать.


                      1. maxzh83
                        06.05.2024 04:16
                        +1

                        Параметры функции передаются через стек, поля объекта создаются в куче. Начните только профайлинг. Много интересного узнаете

                        И что? Сборщик мусора замечательно справляется. В любом джава-приложении создается огромное число классов в том числе даже самой JVM и это не является проблемой. В общем, похоже на экономию на спичках. Если у вас высоконагруженное приложение с лимитом по памяти, то на java в принципе не надо его писать.

                        Ад бесконтрольного роста количества классов с единственной целью удовлетворить какую то одну функцию

                        Он вполне контролируемый) И цель тут сделать код читаемым. Если вам кажется, что applyArg1 и applyArg2 понятнее чем setUser и setEmail, ну ок. В любом подходе надо включать голову, конечно же.

                        а нахрена теперь Ломбок, если есть рекорды?

                        Я ж не знаю какая у вас Java. Рекорды еще лучше, да. Правда, с ними билдер сложно сделать. А если 10 параметров в конструктор пихать, то зачем такой рекорд нужен.

                        Ей Богу, когда меня начинают убеждать, что сроки сорваны из за набора сеттеров и геттеров, которые все равнo IDE сгенерировала

                        Не очень понял при чем тут это, но не суть. Вы просили пояснить почему мне кажется, что каррирование и остальное - не очень, я пояснил. Дальше спорить не вижу смысла


    1. KReal
      06.05.2024 04:16

      Ну. У нас есть JVM, нам нужна функциональщина... даже я, дотнетчик, сразу думаю о Scala.


      1. aegisql Автор
        06.05.2024 04:16

        А я нет. Для легковесной функциональщины достаточно того что есть в Java, плюс минус некоторые расшитрения, типа обсуждаемый. Для тяжелой артиллерии есть Кложур. Скала рядом не валялась.


      1. Sigest
        06.05.2024 04:16
        +1

        К сожалению не всегда можно выбрать язык на проекте. А так да, для JVM куча функциональных языков, где такие выкрутасы лепить не надо, все красиво и понятно


      1. sshikov
        06.05.2024 04:16

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

        Вот там где я могу, я ее легко и непринужденно применяю, и меня мое посредственное ее знание не сильно смущает. Насчет знания - я где-то в top 95% сдававших тест по ней на Linkedin. В итоге я оцениваю рынок скалистов как весьма странный и ненадежный, если можно так выразиться.


  1. sshikov
    06.05.2024 04:16
    +1

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


    1. TerraV
      06.05.2024 04:16

      Согласен, иначе получается "У нас есть 14 конкурирующих библиотек, давайте напишем одну им на замену". В результате очередная инхаус поделка которой пользуется полтора разработчика.


  1. AlekseyShibayev
    06.05.2024 04:16

    1. Не хватает красивого примера боевого кода.

    2. Смотрю я на functional N и думаю, у вас голая java в проекте? Spring и mupstruct не используете? В дефолтный .map() стрима или опшионала передаешь бин с понятным методом мапинга...


    1. aegisql Автор
      06.05.2024 04:16

      Не вполне понимаю ваш вопрос. Этот данный проект полностью независим от каких либо иных библиотек и написан на ванильной Java. А дальше, можно использовать как угодно.

      И да, любой конструктор с N параметрами эквивалентен FunctionN

      Function<Integer,ArrayList> new_list = ArrayList::new;


      1. AlekseyShibayev
        06.05.2024 04:16
        +1

        Поясню.

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

        Потому что с моей колокольни, прикладного, боевого, ентерпрайз-продакшн разработчика, когда spring boot и mapstruct "стандарт" в отрасли - не видно кейсов применения данной библиотеки. Поэтому удивлённо спрашиваю, у вас в ваших проектах, голая java?


        1. aegisql Автор
          06.05.2024 04:16

          У меня проектов десятки. Где то используется спринг, где то нет. Где то есть Ломбок, где то нет. Давать пример продакшн кода невозможно по многим причинам. 1) Нельзя. 2) Он слишком большой для статьи. 3) Придется писать еще две стать с обоснованием, почему принятое решение оптимально. А иначе набегут спецы давать советы. Что собственно и происходит.

          Как будто кто то с ними спорит, что есть много способов ободрать кролика.

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

          Перейти же целиком на функциональные рельсы мешают внешние обстоятельства и решения.