Альтернатива Java 8: что умеет VAVR
https://t.me/data_analysis_ml - Data Science
В 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"));