Перейти к основному содержимому

Введение в Atlassian Fugue

· 9 мин. чтения

1. Введение

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

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

2. Начало работы с фугой

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

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

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

3. Вариант

Давайте начнем наше путешествие с изучения класса 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. Карта Операция _

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

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

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

3.2. Опция и нулевое значение

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

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

Option.some(null);

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

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

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

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

3.3. Вариант I повторяемый ``

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

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

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

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

3.4. Преобразование опции в поток

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

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

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

3.5. java.util. Дополнительное взаимодействие

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

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

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

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

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

Кроме того, он имеет несколько вариантов метода подъема , который поднимает Function<A,B> в Function<Option<A>, Option<B>> :

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. Построение любого

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

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

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

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

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

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

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

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

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

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

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

4.3. Прогнозы

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

Вот где проекции пригодятся.

Левая и правая проекции — это зеркальные представления для Либо , которые фокусируются на левом или правом значении соответственно:

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

В приведенном выше фрагменте кода, если значение «Left» равно «Left», функция decodeSQLErrorCode () будет применена к базовому элементу. Если любой из них прав, он не будет. То же самое и наоборот при использовании правильной проекции.

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

Как и в случае с Options , Fugue также предоставляет класс, полный утилит для Both , и он так и называется :

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

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

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

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

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

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

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

5.1. Создание попытки

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

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

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

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

Checked.Function<String, Object, Exception> throwException = (String x) -> {
throw new Exception(x);
};

assertTrue(Checked.lift(throwException).apply("ko").isFailure());

5.2. Работа с попыткой

Когда у нас есть Try , три наиболее распространенных вещи, которые мы, возможно, захотим с ним сделать, это:

  1. извлечение его значения
  2. привязка некоторой операции к успешному значению
  3. обработка исключения с помощью функции

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

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

Чтобы извлечь значение, мы используем метод getOrElse :

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

assertTrue(failure.isFailure());

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

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. Другие утилиты

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

6.1. Пары

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

Pair<Integer, String> pair = Pair.pair(1, "a");

assertEquals(1, (int) pair.left());
assertEquals("a", pair.right());

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

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

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

6.2. Ед. изм

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

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

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

Удивительно, однако, что Option не понимает Unit , считая его некоторым значением, а не никаким.

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

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

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

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

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

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

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

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

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

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

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