Введение в Atlassian Fugue

Введение в Atlassian Fugue

1. Вступление

Fugue - это библиотека Java от Atlassian; это набор утилит, поддерживающихFunctional Programming.

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

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

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


    io.atlassian.fugue
    fugue
    4.5.1

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

3. Optionс

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

Как мы можем догадаться по названию,Option's a container representing a potentially absent value.

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

Option none = Option.none();
assertFalse(none.isDefined());

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

Option maybe = Option.option(someInputValue);


3.1. Операцияmap

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

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

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

3.2. Option и значениеNull

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

We cannot directly create a non-empty Option holding a null value:

Option.some(null);

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

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

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


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

3.3. Option равноIterable

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

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

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

Option some = Option.some("value");
Iterable 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 Совместимость

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

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



3.6. Класс полезностиOptions

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

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

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

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

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

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

Обратите внимание, что, как и методmap,lift doesn’t map null to None:

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

4. Either для вычислений с двумя возможными исходами

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

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

КлассEither охватывает этот вариант использования.

ЭкземплярEither может бытьRight илиLeft but never both at the same time.

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

4.1. ПостроениеEither

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

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

Either right = Either.right("value");

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

Either left = Either.left(-1);

Здесь наше вычисление может либо вернутьString, либоInteger.

4.2. ИспользуяEither

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

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

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

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

4.3. Прогнозы

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

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

Left and right projections are specular views of an Either that focus on the left or right value соответственно:

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

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

4.4. Полезные методы

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

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

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

Мы завершаем нашу экскурсию по типам данных в Fugue другим вариантом, называемымTry.

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

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

Итак,Try может бытьSuccess илиFailure:

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

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

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

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

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

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

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

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

5.2. Работа сTry

Когда у нас есть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. Объединение вызовов после успеха

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

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

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

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

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

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

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

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

Однако эти методы отличаются тем, что по умолчанию их значение равноto recover from the exception, i.e. to produce a successful Try.

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

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

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


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

Если сама функция восстановления выкидывает, результат - еще один неудачныйTry:

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

assertTrue(failure.isFailure());


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

Try 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 - это действительно простая и универсальная структура данных, состоящая из двух одинаково важных компонентов, которые Fugue называетleft иright:

Pair pair = Pair.pair(1, "a");

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

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

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

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

6.2. Unit

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

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

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

Однако довольно удивительно, чтоOption doesn’t understand Unit, treating it like some value instead of none.

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

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

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

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

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

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

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

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

Однако вы можете прочитать о них и многом другом в Fuguejavadocs иsource code.

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

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