Что такое каррирование и как оно используется?

Каррирование (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&lt;Integer, Function<Integer, Integer&gt;> add = a -> b -> a + b;
add.apply(2).apply(3); // 5

Scala делает это проще и естественнее.