Функциональный Kotlin. Часть 2. Каррированные функции и где они обитают

от автора

Введение

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

Определение

Если говорить простым языком, каррирование это преобразование функции нескольких аргументов к набору функций одного аргумента. Таким образом, функция f будет преобразована в fCurried:

fun f(a: A, b: B, c: C) { ... }  fun fCurried(a: A) = fun(b: B) = fun(c: C) { f(a,b,c) }

Тут мы используем анонимные функции, делая возвращаемым значением первой функции (B) → (C) → Unit, а для второй (C) → Unit.

Такой синтаксис кажется немного странным, верно? Для ООП-языков он и правда необычен, однако для людей использующих, например, Haskell, эта конструкция тривиальна, так как в нем каррированными являются вообще все функции.

Выглядит это примерно так:

f :: A -> B -> C -> Nothing

Что-то похожее мы увидим и на Kotlin, если определим функцию не стандартным способом, а как переменную:

val g: (A, B, C) -> Unit = { ... }  val gCurried: (A) -> (B) -> (C) -> Unit = {   a: A -> {      b: B -> {        c: C -> g(a,b,c)     }    }  }

Применяем каррирование правильно

Раз уж я упомянул Haskell, в случае с ним сам компилятор заточен под вычисления именно с такими функциями, однако Kotlin такими плюсами не обладает. Так зачем же они нам?

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

fun foo(a: A, b: B, c: C) = { ... }

Очевидно, что прописывать все аргументы каждый раз — не очень хороший подход. Что же делать?

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

Опыт работы в объектно ориентированной парадигме подсказывает очевидное, но некачественное решение:

class Bar(val a: A, val b: B) {     fun foo(c: C) { ... }   }

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

Тут-то на помощь и приходит каррирование — оно позволяет нам частично определить функцию и использовать ее позднее без создания лишних объектов, усложняющих понимание кода:

val f = foo(a1, b1) val g = foo(a2, b2)  // ... if (expression) f(c1) else g(c1)  // ... if (expression) f(c2) else g(c2)

А что же «под капотом»?

По сути между различными реализациями каррирования разницы почти нет, разве что можно упереться в ограничения конкретно ФВП или обычных функций, разница между которыми пусть и незначительна, но все же присутствует. Тем не менее, мы в любом случае имеем дело с объектами классаKFunction

P.S.

В этот раз статья вышла довольно короткой, но и сама тема достаточно узка. Следующая (на любую из двух тем в голосовании) будет по объему на уровне первой.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
О чем написать следующую?
44.44% Последовательности (секвентивные алгоритмы) 4
88.89% Монады 8
Проголосовали 9 пользователей. Воздержавшихся нет.

ссылка на оригинал статьи https://habr.com/ru/post/649817/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *