Наконец, 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 () для возвращаемой функции с разными базовыми зарплатами.