Альтернатива Java 8: что умеет VAVR

В 2012 году на одной из первых должностей в качестве Java-разработчика мне посчастливилось работать в команде, которая была очень прогрессивна в плане подходов к программированию. Они, в частности, познакомили меня с парным программированием и разработкой на основе тестов.

Я интересовался Java 8 и функциональным программированием. Настолько, что, когда приходил домой с работы, пытался рефакторить фрагменты кода из некоторых репозиториев, переписывая их с декларативного стиля Java на функциональный стиль Java 8.

В офисе я говорил об этом с коллегами, и однажды мы спросили руководство, можно ли досрочно внедрить Java 8 и обновить наши приложения. К сожалению, столь быстрое обновление до Java 8 не было приоритетом для бизнеса. Но разочарование длилось недолго. Один коллега из другой команды сказал, что их команда получила такой же ответ, но они начали использовать VAVR.io вместо обновления до Java 8.

Это была отличная новость, и, поскольку не было никаких ограничений относительно библиотек, мы сразу же внедрили библиотеку VAVR. Проходили дни и недели, и я учился программировать функционально с помощью VAVR. Мне это очень понравилось, и только почти два года спустя я впервые перешел к профессиональному программированию на Java 8.

VAVR, который много лет назад назывался JavaSlang,  —  это Java API, который привносит возможности функционального программирования в код, а также предоставляет отличный API для неизменяемых коллекций. В этой статье рассмотрим обычный код на Java и его эквивалент с VAVR, чтобы вы увидели, как приятно работать с этой библиотекой.

Функция N

Java 8 поддерживает Function и ByFunction, но Vavr поддерживает типы Function(N), что позволяет принимать до восьми параметров.

Function1<Integer, Integer> foo = (x) -> x + x;

Function8<Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer> bar = (x1 ,x2, x3, x4 ,x5, x6 ,x7, x8) ->
x1 + x2 + x3 + x4 + x5+ x6 + x7+ x8;

Составные функции

С помощью VAVR можно создавать композиты функций. Это весьма удобная фича, которая позволяет расширять функциональность удобным для обслуживания способом:

Function1<String, String> greeting = (s) -> "Hey " + s + "!";
Function1<String, String> toUpperCase = (s) -> s.toUpperCase();
Function1<String, String> withEmphasis = (s) -> s + "!!!!!";
Function1<String, String> bigGreeting = greeting.compose(toUpperCase.compose(withEmphasis));

Лифтинг

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

Function1<String, String> greeting = (s) -> "Hey " + s + "!";
Function1<String, String> toUpperCase = (s) -> s.toUpperCase();
Function1<String, String> withEmphasis = (s) -> {
{
if(s.isEmpty()) {
throw new IllegalArgumentException("Cannot be empty");
}
return s + "!!!!!";
}
};
Function1<String, String> bigGreeting = greeting.compose(toUpperCase.compose(withEmphasis));

bigGreeting.apply("");// Эта строка вызовет исключение, но лифтинг обработает его
Function1<String, Option<String>> liftedGreeting = Function1.lift(bigGreeting);

Частичное применение

Мы можем частично применить функцию, передав ей меньше параметров, чем требуется.

Function2<String, String, String> greet = (s1, s2) -> String.format("%s %s!", s1, s2);

//функция требует два параметра, но мы применяем только один
Function1<String, String> spanishGreet = greet.apply("Hola");
Function1<String, String> frenchGreet = greet.apply("Salut");

System.out.println(spanishGreet.apply("Cecilia"));
System.out.println(frenchGreet.apply("Cecile"));

Каррирование

Каррирование позволяет разложить функцию с несколькими аргументами на последовательность функций с одним аргументом:

Function3<Integer, Integer, Integer, Integer> baseFunction = (a, b, c) -> a + b + c;
Function1<Integer, Function1<Integer, Integer>> part1 = baseFunction.curried().apply(2);
Function1<Integer, Integer> part2 = part1.curried().apply(3);

Integer part3 = part2.curried().apply(1);

Запоминание/идемпотентность

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

 public void example() {

Function1<Integer, String> foo = Function1.of(this::aVeryExpensiveMethod).memoized();

long startFirstExecution = System.currentTimeMillis();
System.out.print(foo.apply(2));
long endFirstExecution = System.currentTimeMillis();
System.out.println(" in " + (endFirstExecution - startFirstExecution) + "ms");

long startSecondExecution = System.currentTimeMillis();
System.out.print(foo.apply(2));
long endSecondExecution = System.currentTimeMillis();
System.out.println(" in " + (endSecondExecution - startSecondExecution) + "ms");
}

private String aVeryExpensiveMethod(Integer number) {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "returned " + (number + number);
}

Option

На мой взгляд, Option из VAVR превосходит свой Java-аналог Optional.

Вот несколько аргументов в пользу этого.

  • В VAVR Option является сериализуемым в отличие от Optional в Java 8.
  • Option в VAVR поддерживает peek(), что позволяет выполнить действие, если что-то нашлось.
  • Option совместим с Optional из Java 8.
  • В VAVR вызов Option.map() может привести к Some(null), что может приводить к исключению NullPointerException. Некоторым такое не нравится, но на самом деле это заставляет обращать внимание на возможные случаи null и обращаться с ними соответствующим образом вместо того, чтобы неосознанно принимать их как должное. Правильный способ справиться с вхождениями null  —  это пользоваться flatMap.

Option  —  это обертка для значений, и при правильном его применении можно избежать проверок на null, а также исключения NullPointerException.

// peek()
Option<String> o1 = Option.of("something");
o1.peek(System.out::println);

// Совместимость
Option<String> o2 = Option.ofOptional(Optional.of("something"));

// flatMap()
Option<String> o3 = o1.map(s -> (String)null);
o3.flatMap(o -> Option.of(o));

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

Try

Try  —  это альтернативный способ обработки исключений, который намного гибче, чем классическая обработка исключений в Java.

  • Мы можем вернуть Try из метода, чтобы отложить его выполнение, и здесь очень интуитивно понятный синтаксис.
  • В случае ошибок мы можем предоставить альтернативные пути выполнения.
  • Если вызванный метод возвращает несколько исключений, а мы хотим выборочно отреагировать на одно из них, можно воспользоваться методом Try recoverWith().
  • Комбинируя Try.sequence и flatmap, мы можем извлечь значения из списка нескольких Try.
//Мы определяем, когда выполнить Try. Если хотим отложить его, то возвращаем Try
Function1<Integer, Integer> something = (x) -> x * 2;
Integer success = Try.of(() -> something.apply(2)).getOrElse(-1);


//Альтернативные пути выполнения для ошибок
Function1<Integer, Integer> somethingBad = (x) -> {throw new RuntimeException();};
Integer failure = Try.of(() -> somethingBad.apply(2)).getOrElse(-1);

//Избирательная реакция
Function1<Integer, Integer> manyBadThings = (x) -> multipleExceptions();
Integer recovered = Try.of(() -> manyBadThings.apply(2))
.recoverWith(IllegalArgumentException.class, Try.of(() -> 777))
.getOrElse(-1);
System.out.println(recovered);

//Комбинируя sequence и flatmap, извлекаем значения из List<Try<String>>
List<Try<String>> tries = List(Try.of(() -> "A"),Try.of(() -> "B"),Try.of(() -> "C"));
Try<String> strings = Try.sequence(tries).flatMap((e) -> e.toTry());

Ленивая инициализация

Лениво инициализированное значение будет вычисляться только единожды, даже если вызывается несколько раз.

Lazy<Integer> lazyValue = Lazy.of(() -> {
System.out.println("too lazy too print many times");
return 123;
});

lazyValue.get();
lazyValue.get();
lazyValue.get();

Коллекции

VAVR предоставляет мощный API неизменяемых коллекций. Потребовалась бы гораздо более обширная статья, чтобы подробнее рассказать обо всех возможностях VAVR Collection, но просто приведу несколько полезных функций:

//Коллекции можно инициализировать несколькими способами
List<Integer> i1 = List.of(1, 2, 3);
List<Integer> i2 = API.List(1, 2, 3, 4);//Хорошо смотрится при статическом импорте

//drop отбросит нужное количество элементов слева направо
List<Integer> droppedValue = i1.drop(2);
System.out.println(droppedValue);

//take позволяет выбрать первые X элементов
List<Integer> takenValues = i1.take(2);
System.out.println(takenValues);

//tail возвращает все, кроме первого элемента
List<Integer> tail = i2.tail();
System.out.println(tail);

//zipWithIndex создает индексы для всех элементов коллекции
List<String> i3 = List.of("A", "B", "C");
System.out.println(i3.zipWithIndex());

//asJava() позволяет преобразовать эти коллекции в коллекции Java 8
java.util.List<Integer> javaIntegers = i1.asJava();

//Коллекции в vavr неизменяемые. Обратите внимание, что у списка нет метода добавления.
//Вместо этого есть append и prepend.
List<Integer> i4 = i1.append(9);
i1.prepend(3);

Кортежи

Кортежи  —  это группы элементов. Java 8 поддерживает пары, но VAVR делает еще шаг вперед и дает доступ к кортежам. Доступ к значениям кортежей осуществляется с использованием значений вида “_1”, “_2” и так далее. Максимальный размер кортежа составляет восемь элементов.

Tuple.of("A");
Tuple.of("A", 2);
Tuple.of("A", 2, true);
Tuple.of("A", 2, true, 0.1D);
Tuple.of("A", 2, true, 0.1D, new Object());
Tuple.of("A", 2, true, 0.1D, new Object(), 'x');
Tuple.of("A", 2, true, 0.1D, new Object(), 'x', 111L);
Tuple.of("A", 2, true, 0.1D, new Object(), 'x', 111L, 2.78F);

Tuple2<String, String> person = Tuple.of("Djordje", "Programmer");
System.out.println(person._1);
System.out.println(person._2);

Проверяемые исключения

Это классическая проблема при использовании лямбд и проверяемых исключений. Блок catch приходится встраивать таким способом. В качестве альтернативы можно провести рефакторинг. Но это просто выглядит некрасиво.

List<String> urls = API.List("zzz", "http://www.google.com", "xx");

urls.map(u -> {
try {
return new URI(u);
} catch (URISyntaxException e) {
e.printStackTrace();
}
return null;
});

С VAVR можно сделать нечто подобное, чтобы реализовать проверяемые исключения:

List<URI> uris = urls.map(u -> API.unchecked(() -> new URI(u)).apply());

Соответствие образцу

С помощью VAVR мы можем выполнять сопоставление с образцом в качестве альтернативы оператора switch:

Object a = 23;

String value = Match(a).of(
Case($(instanceOf(String.class)), "it's a word"),
Case($(instanceOf(Integer.class)), "it's a number"));

Читайте также:

Читайте нас в TelegramVK и Дзен


Перевод статьи JAVING: “I ❤ VAVR”

Предыдущая статья3 интерактивных инструмента для управления командами в Linux
Следующая статья11 инструментов для ускорения создания пользовательского интерфейса