Comments 43
Новаторский? Этому подходу в Java уже лет так… 10 пожалуй. Кому надо — те давно уже его использовали.
Проблематично, само собой. Более того, некоторые вещи вообще практически не сделать по разным причинам (спасибо type erasure, в частности). Но тем не менее, реализации были, некоторые вещи примерно 2008 года вполне себе гуглятся без проблем.
Ну вот скажем, “Lazy Error Handling in Java, Part 3: Throwing Away Throws” — это правда не совсем полноценная реализация, а скорее концепт.
Netbeans и Eclipce поддерживают редактирование BPML. Это правда enterprise, и BPML выполнял сервер, но некоторые библиотеки свободно генерили читаемую JAVA на выходе.
Также, очень многие реализовывали диаграммы во время бума workflow engines (Jira и др).
Но в те времена не был популярным термин функциональное программирование, диаграммы больше описывали DataFlow.
Это всё замечательно, конечно, но мой полусонный мозг не может понять — чем описанная конструкция принципиально отличается от Java Streams? Кроме того, что Streams уже есть в самом языке.
Стримы и стрелки — это два разных подхода к композиции программы из кусков. Из стрелок можно например построить обработку ошибок (Either<Result, Exception> это тоже стрелки, если что).
Т.е. я бы ответил на ваш вопрос так — они всем отличаются. То что вы увидели в примере нечто, показавшееся вам похожим на стримы — чистое совпадение, такой пример автор выбрал.
То есть это какбы библиотека, которая предлагает определённый подход к композиции функций.
Вот я всё равно не вижу отличия. Можно конкретно посмотреть на существующий интерфейс java.util.function.Function
, который очень похож на вашу стрелку. В нём уже есть join
(только называется по-другому — andThen
).
А абстрактные пары и прочие туплы — это, конечно, ад. В джаве оно не нужно.
А давайте, расскажите, как вы скажем будете делать Map, у которого ключом является пара значений разных типов? Вместо tuple — отдельный класс поди заведете?
Естественно! Вы так говорите, будто класс — это что-то плохое.
Может, вы и анонимные классы по-прежнему предпочитаете лямбдам? )
Нет. При чём тут это? Несуразицу вы сейчас сказали.
При том же. Вы предпочитаете написать класс вместо Tuple.of(123, "string")? Даже тогда, когда этот класс не имеет никакого осмысленного имени (а анонимный вы в случае ключа для Map не сделаете), и не имеет никакого смысла вне контекста одной-двух строк кода?
Я именно об этом. Tuple — это ровно такой же безымянный, но с типизированными полями при этом, класс, для которого не имеет смысла придумывать названия, как лябмда — это безымянный класс с одним методом.
Это удобно. Если вы этого не чувствуете — это ваше дело.
Угу, а потом вы захотите этот тупл вернуть из метода и передать в другой метод, потом у вас тупл туплов образуется и понеслась. Никто уже не знает, что значит first, а что значит second. Если вам хочется производить write-only code — это, в принципе, тоже ваше дело.
Ха-ха )))
А потом вы захотите эту лямбду вернуть из метода и передать в другой метод, а потом у вас функции высшего порядка образуются, и понеслась.
Вы все еще не видите аналогии? Пара (или Tuple в общем случае) — это абстракция, которой тоже нужно уметь пользоваться. Написать с ее помощью всякую чепуху можно точно также, как с помощью любой другой абстракции. Но это совершенно не повод утверждать, что В джаве оно не нужно.
Вы никогда не знаете, что внутри у функции Function<Integer, String>, которую вам передали откуда-то, потому что это называется абстракция.
Возможно, это плохая абстракция. Но вы можете сделать более хорошую, например Function<Index, Address>. Все в ваших руках. И иногда Tuples для этого очень удобны.
А есть более реалистичный пример, чем вычисление простой математической формулы, требующее в 10 раз больше кода, чем надо (не считая подключения библиотеки)? Когда это может быть оправдано по сравнению с альтернативами уже имеющимися в языке?
Кстати решил переписать один из математических примеров в обычном стиле:
@FunctionalInterface
interface Function2 <A, B, R> {
public R apply (A a, B b);
}
...
Function2 <Double, Double, Double> sum_SinCos = (a, b) -> {
double sin_res = Math.sin(a);
double cos_res = Math.cos(b);
return sin_res*sin_res + cos_res*cos_res;
};
Не сказал бы, что он в 10 раз короче, тем более тут мы лишились возможности повторного использования блоков.
Вы неправильно это делаете:
static double sumSinCos(double a, double b) {
double sin_res = Math.sin(a);
double cos_res = Math.cos(b);
return sin_res*sin_res + cos_res*cos_res;
}
Если блоком вы считаете вызов метода Math.sin
, то почему мы лишились возможности вызвать Math.sin
в других местах?
Блок — это sin_cos
(который определён как Action.of(Math::sin).combine(Math::cos)
), который можно применять к паре значений.
В вашем примере sumSinCos
— не лямбда, так что он уже не совсем эквивалентен: нужно ещё описать класс-обёртку. В итоге количество строк такое же.
Зачем класс-обёртка? У вас и так уже есть какой-то класс, в нём можно всё написать. Зачем обязательно лямбда? Если где-то принимающая сторона ожидает функциональный интерфейс, воспользуйтесь ссылкой на метод, а если где-то нужно просто вызвать метод, то и вызывайте спокойно. Выглядит так, будто из ничего изобретаются проблемы, а потом они отважно решаются.
А как, отвлекаясь от простых математических функций из примера, из статического метода сделать closure? Статический метод — это уже не эквивалент лямбды и примера со стрелкой.
Проблемы, решаемые данным подходом, показаны в начале статьи: явное описание dataflow в приложении. Если Ваc смущает, что простые математические функции можно вызвать проще, то можно написать более абстрактный пример:
Arrow<Pair<A, B>, С> someArrow = Action.of(f1).combine(f2)
.join(arr2.combine(arr2))
.join(f3);
someArrow.apply(Pair.of(instA, instB));
Тут f1, f2, arr2, f2
— какие-то существующие длинные функции, как это обычно и бывает. В безточечной записи с обычными вызовами это будет выглядеть примерно так (описание интерфейса лямбды опустим):
Function someLambda = (A instA, B instB) -> {
return f3(arr2(f1(instA)), arr2(f2(instB)));
}
someLambda.apply(instA, instB);
Для простых функций оно выглядит почти одинаково, но что, если мы захотим использовать монады или сделать параллельный combine
, например? Со стрелкой мы просто используем ParallelAction вместо Action, а вот лямбду придётся переписывать с futures со всеми вытекающими.
Во, теперь аргументация звучит убедительнее. Конечно, лямбда попроще будет выглядеть:
BiFunction<A, B, C> fn = (instA, instB) -> f3...;
И функциональный интерфейс объявлять, конечно, не нужно, всё есть в стандартной библиотеке. Но не суть. Автоматическое распараллеливание — окей, принимается. А что значит "захотим использовать монады"? Приведите пример.
Зато придётся описывать интерфейс, если там будет больше аргументов. Что мы пишем BiFunction
, что Arrow
— какой-то существенной разницы в объёме написанного не видно, честно говоря. Разве что более многословно описывается последовательность вызова функций, но и возможностей больше.
Работа с монадами в jArrows
пока не реализована из коробки, в статье по ссылкам это называется Kleisli Arrow. С такой стрелкой можно делать композицию функций, которые возвращают монады.
Вы не замечаете тут простую разницу, что при помощи стрелок математические формулы строятся в том числе в runtime? Т.е., по сути, в случае формулы в коде, компилятор за вас построит что-то похожее на стрелки, с выводом типов и т.п.
Окей, добавив много кода, можно сэмулировать AST-дерево формул прямо из джава сорцов. При этом превращая все формулы в довольно-таки нечитаемое месиво. Но если это действительно нужно, почему бы не подключить символьный математический процессор, который будет в рантайме разбирать ваши формулы (и при необходимости делать с ними преобразования — дифференцировать или интегрировать символьно, например)? При этом формулы можно будет писать по-человечески, не теряя в читабельности. Каждой задаче свой инструмент.
В LabVIEW это работает примерно вот так:
как говорится, «как слышится, так и пишется».
Код с синусом и косинусом будет соответственно выглядеть ну как-то вот так, что ли:
Ну или ещё проще:
Стрелки как подход к представлению систем на Java