Функциональное программирование в массы!

Мем с подписью «A monad is just a monoid in the category of endofunctors»

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

Функтор — это «контейнер», который поддерживает функцию fmap() (устоявшееся название). Она позволяет применить к каждому элементу внутри контейнера переданную функцию без изменения исходной структуры.

Иными словами, если у типа есть реализация fmap, то с ним можно работать как с функтором.

На примере Haskell:

fmap (+1) (Just 2)  -- Just 3
fmap (+1) [1, 2, 3] -- [2, 3, 4]

Java глобально такую абстракцию реализовать не даёт, есть только конкретные классы с таким поведением. Приходят на ум очевидные Optional, Stream и CompletableFuture:

Optional<User> user = findUser();
Optional<String> name = user.map(User::getName);

Монада — развитие идеи функторов. Если Optional.map() позволяет модифицировать содержимое контейнера через функции T -> R, то Optional.flatMap() работает с функциями T -> Optional<R> и убирает лишнюю вложенность.

Для Optional<Address> findAddress(User user):

Optional<Optional<Address>> address = findUser().map(user -> findAddress(user)); // неудобно
Optional<Address> address = findUser().flatMap(user -> findAddress(user));

Да, Optional и функтор, и монада. Монады, в отличие от функторов, позволяют строить цепочки вычислений, где каждая следующая функция возвращает значение в контейнере.

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

Optional<String> city = findUser()
    .flatMap(this::findAddress)
    .map(Address::city);

// vs
User user = findUser();
if (user == null) {
    return null;
}
Address address = findAddress(user);
if (address == null) {
    return null;
}
return address.city();

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

В Haskell поддержка нативная:

sum' :: Int -> Int -> Int -- то же, что и Int -> (Int -> Int)
sum' a b = a + b

print (sum' 2 3)     -- можно так
print ((sum' 2) 3)   -- и так

let addTwo = sum' 2
print (addTwo 10)    -- 12

В Java имитируем это через функцию, которая возвращает функцию:

Integer.sum(2)(3); // нельзя
Integer.sum(2, 3); // только так

Function<Integer, Function<Integer, Integer>> curriedSum = a -> b -> a + b;
System.out.println(curriedSum.apply(2).apply(3)); // 5

Function<Integer, Integer> addTwo = curriedSum.apply(2);
System.out.println(addTwo.apply(10)); // 12

В Kotlin тоже нет каррирования из коробки, но синтаксис имитации приятнее:

fun sum(a: Int, b: Int): Int = a + b
sum(2)(3) // нельзя
sum(2, 3) // только так

val curriedSum: (Int) -> (Int) -> Int = { a -> { b -> a + b } }
println(curriedSum(2)(3)) // 5

val addTwo = curriedSum(2)
println(addTwo(10)) // 12
← Назад к списку статей