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
, три наиболее распространенных вещи, которые мы, возможно, захотим с ним сделать, это:
- извлечение его значения
- привязка некоторой операции к успешному значению
- обработка исключения с помощью функции
Кроме того, очевидно, что отбрасывая 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, поэтому его легко импортировать и запускать как есть.