Функціональне програмування на Java з Vavr

Багато чули про такі функціональні мови, як Haskell і Clojure. Але є й такі мови, як, наприклад, Scala. Він поєднує в собі як ООП, так і функціональний підхід. А що щодо старої доброї Java? Чи можна на ній писати програми в функціональному стилі і на скільки це може бути боляче? Так, є Java 8 і лямбды з стримами. Це великий крок для мови, але цього все ще мало. Чи можна щось придумати в такій ситуації? Виявляється так.

Для початку спробуємо визначити, що означає написання коду в функціональному стилі. По-перше, ми повинні оперувати не змінними і маніпуляціями з ними, а ланцюжками деяких обчислень. По суті, послідовністю функцій. Крім того, у нас повинні бути спеціальні структури даних. Наприклад, стандартні java колекції не підходять. Скоро стане зрозуміло чому.

Розглянемо функціональні структури більш докладно. Будь-яка така структура повинна відповідати як мінімум двом умовам:

  • immutable — структура повинна бути незмінною. Це означає, що ми фіксуємо стан об’єкта на етапі створення і залишаємо його таким до кінця його існування. Явний приклад порушення умови: стандартний ArrayList.
  • persistent — структура повинна зберігатися в пам’яті максимально довго. Якщо ми створили якийсь об’єкт, то замість створення нового з таким же станом, ми повинні використовувати готовий.

Очевидно, що нам потрібно якесь стороннє рішення. І таке рішення є: бібліотека Vavr. На сьогоднішній день це найбільш популярна бібліотека Java для роботи у функціональному стилі. Далі я опишу основні фішки бібліотеки. Багато, але далеко не все, приклади та описи були взяті з офіційної документації.

Основні структури даних бібліотеки vavr

 

Кортеж

Однією з найбільш базових і простих функціональних структур даних є кортежі. Кортеж — впорядкований набір фіксованої довжини. На відміну від списків, кортеж може містити дані довільного типу.

Tuple tuple = Tuple.of(1, "blablabla", .0, 42L); // (1, blablabla, 0.0, 42)

Отримання потрібного елемента відбувається виклику поля з номером елемента в кортежі.

((Tuple4) tuple)._1 // 1

Зверніть увагу: індексація кортежів починається з 1! Крім того, для отримання потрібного елемента ми повинні перетворити наш об’єкт до потрібного типу з відповідним набором методів. У наведеному вище прикладі ми використовували кортеж із 4 елементів, а значить перетворення повинно бути в тип Tuple4. Насправді, ніхто не заважає нам спочатку зробити потрібний тип.

Tuple4 tuple = Tuple.of(1, "blablabla", .0, 42L); // (1, blablabla, 0.0, 42)
System.out.println(tuple._1); // 1

Топ 3 колекцій vavr

Список

Створити список з vavr дуже просто. Навіть простіше, ніж без vavr.

List.of(1, 2, 3)

Що ми можемо зробити з таким списком? Ну по-перше, ми можемо перетворити його в стандартний java список.

final boolean containThree = List.of(1, 2, 3)
.asJava()
.stream()
 .anyMatch(x -> x == 3);

Але насправді в цьому немає великої необхідності, тому що ми можемо зробити, наприклад, так:

final boolean containThree = List.of(1, 2, 3)
 .find(x -> x == 1)
 .isDefined();

Взагалі, у стандартного списку бібліотеки vavr є безліч корисних методів. Наприклад, є досить потужна функція згортки, яка дозволяє об’єднувати список значень по деякому правилу і нейтрального елемента.

// розрахунок суми
final int zero = 0; // нейтральний елемент
final BiFunction<Integer, Integer, Integer> combine
 = (x, y) -> x + y; // функція об'єднання
final int sum = List.of(1, 2, 3)
 .fold(zero, combine); // викликаємо згортку

Тут варто відзначити один важливий момент. У нас є функціональні структури даних, а це значить, що ми не можемо змінювати їх стан. Як реалізований наш список? Масиви нам точно не підходять.

Читайте також  Схожі на ос дрони піднімають тяжкості, допомагаючи собі черевцем

Linked List в якості списку за промовчанням

Зробимо однозв’язний список з неизмеяемыми об’єктами. Вийде приблизно так:

Приклад в коді

List list = List.of(1, 2, 3);

У кожного елемента списку є два основних методи: отримання головного елемента (head) і всіх інших (tail).
Приклад в коді

list.head(); // 1
list.tail(); // List(2, 3)

Тепер, якщо ми хочемо поміняти перший елемент в списку (з 1 на 0), то нам треба створити новий список з переиспользованием вже готових частин.

Приклад в коді

final List tailList = list.tail(); // отримуємо хвіст списку
tailList.prepend(0); // додаємо елемент на початок списку

І все! Так як наші об’єкти в листі незмінні, ми отримуємо потокобезопасную і переиспользуемую колекцію. Елементи нашого списку можуть бути застосовані в будь-якому місці програми і це абсолютно безпечно!

Чергу

Ще однією вкрай корисною структурою даних є черга. Як зробити чергу для побудови ефективних і надійних програм у функціональному стилі? Наприклад, ми можемо узяти вже відомі нам структури даних: два списки і кортеж.

Приклад в коді

Queue<Integer> queue = Queue.of(1, 2, 3)
.enqueue(4)
 .enqueue(5);

Коли перший закінчується, ми розгортаємо другий і використовуємо його для читання.


Важливо пам’ятати, що чергу повинна бути незмінною, як і всі інші структури. Але яка користь від черги, яка не змінюється? Насправді, є хитрість. В якості прийнятого значення черзі ми отримуємо кортеж з двох елементів. Перший: потрібний елемент черги, другий: те, що стало з чергою без цього елемента.

System.out.println(queue); // Queue(1, 2, 3, 4, 5)
Tuple2<Integer, Queue<Integer>> tuple2 = queue.dequeue();
System.out.println(tuple2._1); // 1
System.out.println(tuple2._2); // Queue(2, 3, 4, 5)

Стріми

Наступна важлива структура даних — це стрім. Стрім являє собою потік виконання деяких дій над деякими, часто абстрактним, набором значень.
Хтось може сказати, що в Java 8 вже є повноцінні стріми і нові нам зовсім не потрібні. Чи це Так?
Для початку, давайте переконаємося, що java stream — не функціональна структура даних. Перевіримо структуру на змінність. Для цього створимо такий невеликий стрім:

IntStream standardStream = IntStream.range(1, 10);

Зробимо перебір всіх елементів в стриме:

standardStream.forEach(System.out::print);

У відповідь отримуємо вивід на консоль: 123456789. Давайте повторимо операцію перебору:

standardStream.forEach(System.out::print);

Упс, сталася така помилка:

java.lang.IllegalStateException: stream has already been operated upon or closed

Справа в тому, що стандартні стріми — це просто деяка абстракція над ітератором. Хоч стріми зовні і здаються вкрай незалежними і потужними, але мінуси ітераторів нікуди не поділися.
Наприклад, у визначенні стріму нічого не сказано про обмеження кількості елементів. На жаль, в итераторе воно є, а значить є і стандартних стримах. Ми не можемо створювати нескінченні структури.
На щастя, бібліотека vavr вирішує ці проблеми. Переконаємося в цьому:

Stream stream = Stream.range(1, 10);
stream.forEach(System.out::print);
stream.forEach(System.out::print);

У відповідь отримуємо 123456789123456789. Що означає перша операція не “зіпсувала” наш стрім.
Тепер спробуємо створити нескінченний стрім:
Stream infiniteStream = Stream.from(1);
System.out.println(infiniteStream); // Stream(1, ?)
Зверніть увагу: при друку об’єкта ми не отримуємо нескінченну структуру, а перший елемент і знак питання. Справа в тому, що кожен подальший елемент в стриме генерується нальоту. Такий підхід називається ледачою ініціалізацією. Саме він і дозволяє безпечно працювати з таким структурами.
Якщо ви ніколи не працювали з нескінченними структурами даних, то швидше за все ви думаєте: навіщо взагалі це потрібно? Але вони можуть бути дуже зручні. Напишемо стрім, який повертає довільну кількість непарних чисел, перетворює їх в рядок і додає пробіл:

Stream oddNumbers = Stream
 .from(1, 2) // від 1 з кроком 2
 .map(x -> x + " "); // форматування
// приклад використання
oddNumbers.take(5)
 .forEach(System.out::print); // 1 3 5 7 9
oddNumbers.take(10)
 .forEach(System.out::print); // 1 3 5 7 9 11 13 15 17 19

Ось так просто.

Читайте також  Математична модель фонеми людського голосу

Загальна структура колекцій

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

Кожен елемент структури може бути використаний як итерируемый:

StringBuilder builder = new StringBuilder();
for (String word : List.of("one", "two", "tree")) {
 if (builder.length() > 0) {
 builder.append(", ");
}
builder.append(word);
}
System.out.println(builder.toString()); // one, two, tree

Але варто двічі подумати і подивитися доку перед використанням for. Бібліотека дозволяє робити звичні речі простіше.

System.out.println(List.of("one", "two", "дерево").mkString(", ")); // one, two, tree

Робота з функціями

Бібліотека має ряд функцій (8 штук) і корисні методи роботи з ними. Вони являють собою звичайні функціональні інтерфейси з безліччю цікавих методів. Назва функцій залежить від кількості прийнятих аргументів (від 0 до 8). Наприклад, Function0 не приймає аргументів, Function1 приймає один аргумент, Function2 приймає два і т. д.

Function2<String, String, String> combineName =
 (lastName, firstName) -> firstName + "" + lastName;
System.out.println(combineName.apply("Griffin", "Peter")); // Peter Griffin

У функціях бібліотеки vavr ми можемо робити дуже багато крутих речей. По функціоналу вони йдуть далеко вперед від стандартних нам Function, BiFunction і т. д. Наприклад, каррирование. Каррирование — це побудова функцій по частинах. Подивимося на прикладі:

// Створюємо базову функцію
Function2<String, String, String> combineName =
 (lastName, firstName) -> firstName + "" + lastName;
// На основі базової будуємо нову функцію з одним переданим елементом
Function1<String, String> makeGriffinName = combineName
.curried()
.apply("Griffin");
// Працюємо як з повноцінною функцією
System.out.println(makeGriffinName.apply("Peter")); // Peter Griffin
System.out.println(makeGriffinName.apply("Lois")); // Lois Griffin

Як ви бачите, досить лаконічно. Метод curried влаштований дуже просто, але може принести величезну користь.
Реалізація методу curried

@Override
default Function1<T1, Function1<T2, R>> curried() {
 return t1 -> t2 -> apply(t1, t2);
}

У наборі Function є ще безліч корисних методів. Наприклад, можна кешувати зворотний результат функції:

Function0<Double> hashCache =
Function0.of(Math::random).memoized();

double randomValue1 = hashCache.apply();
double randomValue2 = hashCache.apply();

System.out.println(randomValue1 == randomValue2); // true

Боротьба з винятками

Як ми говорили раніше, процес програмування повинен бути безпечним. Для цього необхідно уникати різних сторонніх ефектів. Виключення (exceptions) є явними їх генераторами.

Читайте також  Ще раз про затримки у вихідному коді проекту FPGA або просте питання для співбесіди на вакансію розробника FPGA

Для безпечної обробки виключень в функціональному стилі можна використовувати клас Try. Насправді, це типова монада. Заглиблюватися в теорію для використання зовсім не обов’язково. Досить подивитися простий приклад:

Try.of(() -> 4 / 0)
.onFailure(System.out::println)
 .onSuccess(System.out::println);

Як видно з прикладу все досить просто. Ми просто вішаємо подія на потенційну помилку і не виносимо її за межі обчислень.

Pattern matching

Часто виникає ситуація, в якій нам необхідно перевіряти значення змінної і моделювати поведінку програми в залежності від результату. Якраз у таких ситуаціях на допомогу приходить чудовий механізм пошуку за шаблоном. Більше не треба писати купу if else, достатньо налаштувати всю логіку в одному місці.

final int i = 1;
String s = Match(1993).of(
 Case($(42), () -> "one"),
 Case($(anyOf(isIn(1990, 1991, 1992), is(1993))), "two"),
 Case($(), "?")
);
System.out.println(s); // one

Зверніть увагу, Case написано з великої літери, т. к. case є ключовим словом і вже зайнято.

Висновок

На мій погляд бібліотека дуже крута, але варто застосовувати її вкрай акуратно. Вона може відмінно проявити себе в event-driven розробці. Однак, надмірне і бездумне її використання в стандартному імперативний програмуванні, заснованому на пулі потоків, може принести багато головного болю. Крім того, часто в наших проектах використовуються Spring і Hibernate, які не завжди готові до такого застосування. Перед імпортом бібліотеки в свій проект необхідно чітке розуміння, як і навіщо вона буде використана. Про що я і розповім в одній із своїх наступних статей.

Степан Лютий

Обожнюю технології в сучасному світі. Хоча частенько і замислююся над тим, як далеко вони нас заведуть. Не те, щоб я прям і знаюся на ядрах, пікселях, коллайдерах і інших парсеках. Просто приходжу в захват від того, що може в творчому пориві вигадати людський розум.

You may also like...

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *