Наконец, Java с ее восьмым основным выпуском получит лямбда-выражения, а затем станет более функциональным. Вместе с Раулем-Габриэлем Урмой и Аланом Майкрофтом я начал писать книгу на эту тему. В любом случае простого использования лямбда-выражения недостаточно, чтобы утверждать, что вы занимаетесь функциональным программированием и, что более важно, использовать его возможности. Теперь Java поддерживает новую парадигму программирования, а затем люди, которые, как и я, занимались императивным объектно-ориентированным программированием на Java в течение десятка лет, должны сделать довольно большой сдвиг в уме, чтобы эффективно использовать эти новые методы. Например, в предыдущей статьеЯ предположил, что пришло время прекратить использовать нулевую ссылку или, по крайней мере, начать использовать ее более экономно, и заменить ее более безопасной и более явной конструкцией, такой как Option.
На мой взгляд, хороший способ научиться использовать лямбда-выражения в работе более идиоматическим способом — это читать статьи, написанные на других функциональных языках, лучше Scala, поскольку они наиболее похожи на Java, и пытаться переопределить их в Java 8 образцы и решения, предложенные там. Это то, что я сделал с настоящим постом, который является просто переводом на Java статьи Scala, написанной Дебасишем Гошем.
Автор этой статьи попытался решить следующую, по-видимому, тривиальную проблему: рассчитать чистый оклад сотрудника, начиная с его базового, и последовательно применяя следующие 4 метода, которые добавляют бонусы и вычитают из них налоги.
public class SalaryCalculator {
// B = basic + 20%
public double plusAllowance(double d) {
return d * 1.2;
}
// C = B + 10%
public double plusBonus(double d) {
return d * 1.1;
}
// D = C - 30%
public double plusTax(double d) {
return d * 0.7;
}
// E = D - 10%
public double plusSurcharge(double d) {
return d * 0.9;
}
}
Чтобы сделать проблему немного более интересной, Дебасиш также предположил, что применение всех этих 4 бонусов или налогов, по одному, необязательно. С этим последним условием простой, но обязательный способ расчета чистой заработной платы может быть следующим:
public double calculate(double basic, boolean... bs) {
double salary = basic;
if (bs[0]) salary = plusAllowance(salary);
if (bs[1]) salary = plusBonus(salary);
if (bs[2]) salary = plusTax(salary);
if (bs[3]) salary = plusSurcharge(salary);
return salary;
}
Здесь bs — массив из 4 логических значений (для краткости я пропустил проверку длины массива), где каждый логический параметр указывает, должен ли применяться соответствующий метод или нет. Так, например:
calculate(1000.0, true, false, false, true);
рассчитывает чистую зарплату, начиная с базовой 1000 и применяя только надбавку (1-й метод) и надбавку (4-й). Пока все хорошо, но, как я уже сказал, предлагаемое решение слишком важно для удовлетворения наших новых функциональных предпочтений. Прежде всего, чтобы переписать его в более функциональном стиле, возможно, избавившись от изменяемой переменной заработной платы и 4 условных ветвей, отметим, что все 4 метода, используемые для расчета чистой заработной платы, на самом деле являются специальным типом функции. называется эндоморфизмом, где единственный входной параметр и возвращаемое значение имеют одинаковый тип. В терминах Java эндоморфизм может быть просто определен как:
interface Endomorphism<A> extends Function<A, A> { }
На самом деле в стандартном API Java 8 уже существует интерфейс UnaryOperator, определенный точно так же. Я переименовал его в «Эндоморфизм», потому что считаю это имя более правильным. Однако, если вы предпочитаете, вы можете просто заменить мой интерфейс Endomorphism на стандартный UnaryOperator, один в остальной части статьи, и все остальное будет работать точно так же. Для достижения нашей цели нам также понадобится вторая абстракция — моноид:
interface Monoid<A> {
A append(A a1, A a2);
A zero();
}
В теории категорий моноид определяется как полугруппа с тождеством. На планете Земля это просто ассоциативная операция над типом, который также имеет ноль. Теперь можно объединить эти две абстракции и определить моноид для типа эндоморфизма следующим образом:
interface EndoMonoid<A> extends Monoid<Endomorphism<A>> {
@Override
default Endomorphism<A> append(Endomorphism<A> a1, Endomorphism<A> a2) {
return (A a) -> a2.apply(a1.apply(a));
}
@Override
default Endomorphism<A> zero() {
return a -> a;
}
}
Здесь ассоциативная операция представляет собой композицию двух функций (я не использовал напрямую метод compose (), определенный на интерфейсе функций, который эндоморфизм расширяет только потому, что он возвращает функцию вместо эндоморфизма), а ноль — просто тождественная функция , Это фактически ноль нашего моноида, потому что композиция любого эндоморфизма с ним возвращает сам эндоморфизм. Обратите внимание, что я определил другой интерфейс вместо класса, потому что, используя ключевое слово default, Java 8 позволяет добавить поведение, то есть реализованные методы, в интерфейс при условии, что они не имеют состояния. В конце концов мы можем реализовать этот последний интерфейс с реальным классом, единственная цель которого состоит в том, чтобы инкапсулировать эндоморфизм и предоставить несколько удобных методов, которые позволят нам свободно вычислять чистую зарплату,и конечно же функциональный стиль.
public class FluentEndoMonoid<A> implements EndoMonoid<A> {
private final Endomorphism<A> endo;
public FluentEndoMonoid(Endomorphism<A> endo) {
this.endo = endo;
}
public FluentEndoMonoid(Endomorphism<A> endo, boolean b) {
this.endo = b ? endo : zero();
}
public FluentEndoMonoid<A> add(Endomorphism<A> other) {
return new FluentEndoMonoid<A>(append(endo, other));
}
public FluentEndoMonoid<A> add(Endomorphism<A> other, boolean b) {
return add(b ? other : zero());
}
public Endomorphism<A> get() {
return endo;
}
public static <A> FluentEndoMonoid<A> endo(Endomorphism<A> f, boolean b) {
return new FluentEndoMonoid<A>(f, b);
}
}
В частности, метод add (), принимающий как эндоморфизм, так и логическое значение, объединяет текущий эндоморфизм с предоставленным, если логическое значение истинно или с нулем (тождественная функция), в противном случае. Теперь мы готовы написать функциональную версию прежнего метода расчета:
public double calculate(double basic, boolean... bs) {
return endo((Endomorphism<Double>) this::plusAllowance, bs[0])
.add(this::plusBonus, bs[1])
.add(this::plusTax, bs[2])
.add(this::plusSurcharge, bs[3])
.get()
.apply(basic);
}
Как и ожидалось, основными преимуществами этого второго решения являются отсутствие как изменяемого состояния, так и альтернативной ветви оценки. Тем не менее, есть еще один положительный эффект от этого функционально-управляемого рефактора, который стоит подчеркнуть: удаляя последний вызов apply () из прежней цепочки быстрых вызовов, вы можете получить метод, возвращающий функцию для каждой из 16 возможных комбинаций 4 логического значения. значения в массиве. Таким образом, если, например, вам нужно рассчитать много зарплат, для которых должны применяться только надбавки и надбавки, вы можете получить функцию, передающую этому методу массив [true, false, false, true], а затем вычислить все эти зарплаты, просто вызвав несколько раз apply () для возвращаемой функции с разными базовыми зарплатами.