Что такое каррирование и как оно используется?
Каррирование (currying) — это техника трансформации функции, принимающей несколько аргументов, в цепочку функций, каждая из которых принимает один аргумент. Эта концепция происходит из математической логики и активно используется в функциональном программировании, включая такие языки, как Scala, Haskell, OCaml и F#. В Scala поддержка каррирования встроена в синтаксис языка.
Суть каррирования
Если есть функция f(a, b), то при каррировании она трансформируется в f(a)(b). То есть, вместо передачи всех аргументов сразу, передаётся один аргумент, и возвращается функция, ожидающая следующий.
Пример:
def add(a: Int)(b: Int): Int = a + b
Эта функция принимает сначала a, а потом b, и возвращает их сумму.
Как вызвать каррированную функцию
val sum = add(2)(3) // 5
Также можно частично применить аргументы:
val addTwo = add(2)_ // Функция, ожидающая один аргумент
val result = addTwo(5) // 7
Разница с обычной функцией
Обычная функция:
def multiply(a: Int, b: Int): Int = a \* b
Каррированная версия:
def multiplyCurried(a: Int)(b: Int): Int = a \* b
Обычную функцию можно вызвать только с двумя аргументами одновременно, а каррированную можно вызывать поэтапно.
Применение каррирования
1. Частичное применение функций
Позволяет создавать более специфичные функции из общих.
def greet(prefix: String)(name: String): String = s"$prefix $name"
val sayHello = greet("Hello") _
val sayHi = greet("Hi") _
sayHello("Alice") // "Hello Alice"
sayHi("Bob") // "Hi Bob"
2. Функции высшего порядка
Можно передавать функцию с частично применёнными аргументами как параметр.
val nums = List(1, 2, 3)
nums.map(add(10)) // List(11, 12, 13)
3. Композиция и читабельность
Каррирование упрощает композицию функций, особенно в цепочках трансформаций.
def log(level: String)(message: String): Unit =
println(s"\[$level\]: $message")
val infoLog = log("INFO") _
val warnLog = log("WARN") _
infoLog("Запуск приложения")
warnLog("Низкий объём памяти")
Синтаксис curried и tupled
Scala предоставляет методы curried и tupled на функции:
val addTupled = ( (a: Int, b: Int) => a + b ).curried
val addTwo = addTupled(2) // теперь это Int => Int
addTwo(5) // 7
И наоборот:
val addBack = Function.uncurried(add \_)
addBack(2, 3) // 5
Использование в коллекциях
val numbers = List(1, 2, 3, 4, 5)
def multiplyBy(x: Int)(y: Int): Int = x \* y
val timesTwo = multiplyBy(2) _
val doubled = numbers.map(timesTwo) // List(2, 4, 6, 8, 10)
Разница между множественными списками аргументов и каррированием
Функция с несколькими списками аргументов:
def func(a: Int)(b: Int)(c: Int): Int = a + b + c
Это форма каррирования. Но можно записать и явно каррированную версию:
val curriedFunc = (a: Int) => (b: Int) => (c: Int) => a + b + c
curriedFunc(1)(2)(3) // 6
Обратная сторона: неявность и сложность
Каррирование делает код декларативным и модульным, но может быть менее очевидным для тех, кто привык к обычному способу передачи аргументов. Особенно это касается начинающих программистов, которые не знакомы с концепциями замыканий и частичного применения функций.
Поддержка в стандартной библиотеке
Многие функции из FunctionN в Scala можно каррировать:
val sum = (a: Int, b: Int) => a + b
val curriedSum = sum.curried
curriedSum(3)(4) // 7
Подводные камни
-
Каррирование создает цепочку функций, каждая из которых является новым объектом, что может повлиять на производительность в высоконагруженных системах.
-
При отладке ошибок может быть сложнее понять источник из-за вложенности.
Сравнение с Java
В Java нет встроенного синтаксиса каррирования, хотя можно имитировать его с помощью вложенных лямбд:
Function<Integer, Function<Integer, Integer>> add = a -> b -> a + b;
add.apply(2).apply(3); // 5
Scala делает это проще и естественнее.