«1. Введение

Fugue — это библиотека Java от Atlassian; это набор утилит, поддерживающих функциональное программирование.

В этой статье мы сосредоточимся на наиболее важных API-интерфейсах Fugue и изучим их.

2. Начало работы с Fugue

Чтобы начать использовать Fugue в наших проектах, нам нужно добавить следующую зависимость:

<dependency>
    <groupId>io.atlassian.fugue</groupId>
    <artifactId>fugue</artifactId>
    <version>4.5.1</version>
</dependency>

Мы можем найти самую последнюю версию Fugue на Maven Central.

3. Option

Давайте начнем наше путешествие с изучения класса Option, который является ответом Fugue на java.util.Optional.

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

Другими словами, Option является либо некоторым значением определенного типа, либо None:

Option<Object> none = Option.none();
assertFalse(none.isDefined());

Option<String> some = Option.some("value");
assertTrue(some.isDefined());
assertEquals("value", some.get());

Option<Integer> maybe = Option.option(someInputValue);

3.1. Операция map

Одним из стандартных API-интерфейсов функционального программирования является метод map(), который позволяет применять предоставленную функцию к базовым элементам.

Метод применяет предоставленную функцию к значению Option, если оно присутствует:

Option<String> some = Option.some("value") 
  .map(String::toUpperCase);
assertEquals("VALUE", some.get());

3.2. Option и нулевое значение

Помимо различий в именах, Atlassian сделал несколько вариантов дизайна для Option, которые отличаются от опциональных; давайте теперь посмотрим на них.

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

Option.some(null);

Приведенное выше создает исключение.

Однако мы можем получить его в результате использования операции map():

Option<Object> some = Option.some("value")
  .map(x -> null);
assertNull(some.get());

Это невозможно, если просто использовать java.util.Optional.

3.3. Option Is Iterable

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

Это значительно повышает совместимость при работе с коллекциями/потоками.

А теперь, например, можно объединить с другой коллекцией:

Option<String> some = Option.some("value");
Iterable<String> strings = Iterables
  .concat(some, Arrays.asList("a", "b", "c"));

3.4. Преобразование Option в Stream

Поскольку Option является Iterable, его также можно легко преобразовать в Stream.

После преобразования экземпляр Stream будет иметь ровно один элемент, если опция присутствует, или ноль в противном случае:

assertEquals(0, Option.none().toStream().count());
assertEquals(1, Option.some("value").toStream().count());

3.5. java.util.Optional Interoperability

Если нам нужна стандартная реализация Optional, мы можем легко получить ее с помощью метода toOptional():

Optional<Object> optional = Option.none()
  .toOptional();
assertTrue(Option.fromOptional(optional)
  .isEmpty());

3.6. Вспомогательный класс Options

Наконец, Fugue предоставляет несколько служебных методов для работы с параметрами в классе Options с соответствующим названием.

Он содержит такие методы, как filterNone для удаления пустых опций из коллекции и flatten для превращения коллекции опций в коллекцию вложенных объектов, отфильтровывая пустые опции.

Кроме того, в нем есть несколько вариантов метода подъема, который поднимает функцию\u003cA,B\u003e в функцию\u003cопция\u003cA\u003e, опция\u003cB\u003e\u003e:

Function<Integer, Integer> f = (Integer x) -> x > 0 ? x + 1 : null;
Function<Option<Integer>, Option<Integer>> lifted = Options.lift(f);

assertEquals(2, (long) lifted.apply(Option.some(1)).get());
assertTrue(lifted.apply(Option.none()).isEmpty());

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

Обратите внимание, что, как и метод map, lift не отображает значение null в None:

assertEquals(null, lifted.apply(Option.some(0)).get());

4. Либо для вычислений с двумя возможными результатами

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

Однако иногда нам нужно вернуть больше информации, чем «нет значения»; например, мы можем захотеть вернуть допустимое значение или объект ошибки.

Этот вариант использования охватывает класс Both.

Экземпляр Либо может быть Правым или Левым, но никогда обоими одновременно.

По соглашению, правый результат — это результат успешного вычисления, а левый — исключительный случай.

4.1. Конструирование Либо

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

Мы вызываем right, если хотим, чтобы объект Both содержал значение Right:

Either<Integer, String> right = Either.right("value");

В противном случае мы вызываем left:

Either<Integer, String> left = Either.left(-1);

Здесь наши вычисления могут возвращать либо строку, либо целое число.

4.2. Использование «любого»

Когда у нас есть экземпляр «любого», мы можем проверить, является ли он левым или правым, и действовать соответственно:

if (either.isRight()) {
    ...
}

Что еще интереснее, мы можем связать операции, используя функциональный стиль:

either
  .map(String::toUpperCase)
  .getOrNull();

4.3 . Прогнозы

«Главное, что отличает Либо от других монадических инструментов, таких как Option, Try, заключается в том, что зачастую он беспристрастен. Проще говоря, если мы вызываем метод map(), Либо не знает, работать ли с левой или правой стороной.

Здесь пригодятся прогнозы.

Левая и правая проекции являются зеркальными представлениями для значения Both, которые фокусируются на левом или правом значении, соответственно: элемент. Если любой из них прав, он не будет. То же самое и наоборот при использовании правильной проекции.

either.left()
  .map(x -> decodeSQLErrorCode(x));

4.4. Вспомогательные методы

Как и в случае с Options, Fugue также предоставляет класс, полный служебных программ для Либо, и он так и называется: Либо.

Он содержит методы для фильтрации, приведения и повторения коллекций Либо.

5. Обработка исключений с помощью Try

Мы завершаем наше знакомство с теми или иными типами данных в Fugue еще одним вариантом под названием Try.

Try похож на Choose, но отличается тем, что предназначен для работы с исключениями.

Подобно Option и в отличие от Both, Try параметризуется для одного типа, потому что «другой» тип фиксируется как Exception (в то время как для Option это неявно Void).

Итак, попытка может быть как успешной, так и неудачной:

5.1. Создание экземпляра попытки

assertTrue(Try.failure(new Exception("Fail!")).isFailure());
assertTrue(Try.successful("OK").isSuccess());

Часто мы не будем явно создавать попытку как успех или неудачу; скорее, мы создадим его из вызова метода.

Checked.of вызывает заданную функцию и возвращает Try, инкапсулируя ее возвращаемое значение или любое выброшенное исключение:

Другой метод, Checked.lift, берет потенциально вызывающую функцию и поднимает ее до функции, возвращающей Try :

assertTrue(Checked.of(() -> "ok").isSuccess());
assertTrue(Checked.of(() -> { throw new Exception("ko"); }).isFailure());

5.2. Работа с Try

Checked.Function<String, Object, Exception> throwException = (String x) -> {
    throw new Exception(x);
};
        
assertTrue(Checked.lift(throwException).apply("ko").isFailure());

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

5.3. Извлечение успешного значения

  1. extracting its value
  2. chaining some operation to the successful value
  3. handling the exception with a function

Для извлечения значения мы используем метод getOrElse:

Он возвращает успешное значение, если оно присутствует, или некоторое вычисленное значение в противном случае.

Здесь нет getOrThrow или чего-то подобного, но поскольку getOrElse не перехватывает никаких исключений, мы можем легко написать его:

assertEquals(42, failedTry.getOrElse(() -> 42));

5.4. Цепочка вызовов после успеха

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

someTry.getOrElse(() -> {
    throw new NoSuchElementException("Nothing to get");
});

Это типичный метод карты, который мы находим в Option, Both и большинстве других контейнеров и коллекций:

Он возвращает Try, поэтому мы можем связать дальнейшие операции.

Конечно, у нас также есть разновидность flatMap:

Try<Integer> aTry = Try.successful(42).map(x -> x + 1);

5.5. Восстановление после исключений

У нас есть аналогичные операции сопоставления, которые работают за исключением попытки (если она присутствует), а не ее успешного значения.

Try.successful(42).flatMap(x -> Try.successful(x + 1));

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

Таким образом, мы можем создать новое значение с помощью recovery:

Как мы видим, функция восстановления принимает исключение в качестве единственного аргумента.

Если сама функция восстановления сбрасывает, то результат другой сбой Попробуйте:

Try<Object> recover = Try
  .failure(new Exception("boo!"))
  .recover((Exception e) -> e.getMessage() + " recovered.");

assertTrue(recover.isSuccess());
assertEquals("boo! recovered.", recover.getOrElse(() -> null));

Аналог flatMap называется recoveryWith:

6. Другие утилиты

Try<Object> failure = Try.failure(new Exception("boo!")).recover(x -> {
    throw new RuntimeException(x);
});

assertTrue(failure.isFailure());

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

Try<Object> recover = Try
  .failure(new Exception("boo!"))
  .recoverWith((Exception e) -> Try.successful("recovered again!"));

assertTrue(recover.isSuccess());
assertEquals("recovered again!", recover.getOrElse(() -> null));

6.1. Пары

Пара — это действительно простая и универсальная структура данных, состоящая из двух одинаково важных компонентов, которые Fugue называет левым и правым:

Fugue не предоставляет много встроенных методов для пар, кроме сопоставления и образец аппликативного функтора.

Однако пары используются во всей библиотеке и легко доступны для пользовательских программ.

Pair<Integer, String> pair = Pair.pair(1, "a");
        
assertEquals(1, (int) pair.left());
assertEquals("a", pair.right());

Следующая реализация Лиспа для бедняков находится всего в нескольких нажатиях клавиш!

6.2. Unit

Unit — это перечисление с одним значением, которое предназначено для представления «нет значения».

«Это замена типа возвращаемого значения void и класса Void, который избавляется от null:

Как ни странно, Option не понимает Unit, рассматривая его как некое значение, а не значение null.

6.3. Статические утилиты

Unit doSomething() {
    System.out.println("Hello! Side effect");
    return Unit();
}

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

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

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

Iterables и Iterators, наконец, содержат множество статических методов для управления этими двумя широко используемыми стандартными интерфейсами Java.

7. Заключение

В этой статье мы сделали обзор библиотеки Fugue от Atlassian.

Мы не коснулись сложных по алгебре классов, таких как Monoid и Semigroups, потому что они не подходят для общей статьи.

Однако вы можете прочитать о них и многом другом в javadocs и исходном коде Fugue.

Мы также не коснулись ни одного из дополнительных модулей, которые предлагают, например, интеграцию с Guava и Scala.

Реализацию всех этих примеров и фрагментов кода можно найти в проекте GitHub — это проект Maven, поэтому его легко импортировать и запускать как есть.

«

The implementation of all these examples and code snippets can be found in the GitHub project – this is a Maven project, so it should be easy to import and run as is.