Введение
В прошлой статье мы рассмотрели основу современного функционального программирования - функции высшего порядка. Настало время познакомиться с ними чуть глубже, узнав про каррирование.
Определение
Если говорить простым языком, каррирование это преобразование функции нескольких аргументов к набору функций одного аргумента. Таким образом, функция 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.
В этот раз статья вышла довольно короткой, но и сама тема достаточно узка. Следующая (на любую из двух тем в голосовании) будет по объему на уровне первой.
Комментарии (3)
vsh797
06.02.2022 08:56+1Как раз изучаю Haskell. Там каррирование используется в основном, чтобы частично примененную функцию потом передать в функцию высших порядков. Ну, и для того, чтобы в бесточечном стиле лишние аргументы не пробрасывать. Еще прикольно одну функцию частично применять к другой. Результирующий тип по началу кажется очень неожиданным.
IL_Agent
Где здесь проблема? Каринованные функции - те же объекты.
yhwh0 Автор
Да, каррированные функции действительно являются объектами, как и любые другие функции собственно.
Проблема в том, что это лишний однозадачный класс в проекте, а вместо хранения одной переменной-функции мы теперь храним и объект класса, и еще одну функцию в нем, а она не совсем бесплатная в таком виде.