Определение функций и карринг

В Haskell любая функция технически берёт один аргумент и возвращает функцию. Это называется каррированием — и это суперсила.
Функция add x y на самом деле — это add, которая берёт x и возвращает новую функцию, ждущую y. Поэтому частичное применение работает само собой.

Функция в Haskell определяется до смешного просто: имя, аргументы, знак равенства, тело. Никаких ключевых слов вроде def или function:

double :: Int -> Int
double x = x * 2

add :: Int -> Int -> Int
add x y = x + y

Первая строка — сигнатура типа (её можно не писать, компилятор выведет сам, но писать — хороший тон). Вторая — само определение.

Что скрывает стрелка

Посмотрите на тип add :: Int -> Int -> Int. Стрелка правоассоциативна, то есть читается как Int -> (Int -> Int). Это значит: add берёт один Int и возвращает функцию Int -> Int. Все функции в Haskell — одноаргументные. Это и есть каррирование.

add :: Int -> Int -> Int
      то же самое, что
add :: Int -> (Int -> Int)

add 2       :: Int -> Int        (ждёт второй аргумент)
add 2 3     :: Int               (= 5)

Отсюда — частичное применение: можно дать функции не все аргументы и получить новую функцию.

add :: Int -> Int -> Int
add x y = x + y

addTen :: Int -> Int
addTen = add 10        -- частичное применение!

-- addTen 5  ==  15

Питон тоже умеет частичное применение, просто менее изящно — через замыкания или functools.partial:

# Та же идея на Python: каррирование и частичное применение
def add(x):
    def inner(y):
        return x + y
    return inner

print(add(2)(3))          # 5 — две стрелки как в Haskell

# Частичное применение
add_ten = add(10)
print(add_ten(5))         # 15

from functools import partial
def add2(x, y): return x + y
add_hundred = partial(add2, 100)
print(add_hundred(1))     # 101

Каррирование в повседневном коде

Частичное применение перестаёт быть абстракцией, как только вы начинаете комбинировать функции. Передавая map (add 10), вы прибавляете десятку к каждому элементу, не написав ни одной лямбды. Сравнивая filter (> 0), фильтруете положительные — и снова всё через частичное применение оператора. Именно благодаря каррированию библиотека функций высшего порядка в Haskell такая компактная: map, filter, foldr ожидают на вход функцию одного аргумента, а вы готовите её на месте, недодав остальные. Поэтому опытные хаскелисты подбирают порядок аргументов осознанно: то, что чаще фиксируют заранее, ставят первым, чтобы частичное применение читалось естественно. Это маленькое решение на этапе проектирования сигнатуры окупается каждый раз, когда функцию передают дальше.

Как это мыслить

Каждая стрелка в сигнатуре — это «дайте мне ещё один аргумент». Если вы дали меньше аргументов, чем стрелок, у вас на руках функция, ждущая остальных. Это не редкий трюк, а повседневный инструмент: частично применённые функции отлично передаются в map, filter и подобные.

Есть и тонкость, о которой полезно знать заранее: иногда удобный для частичного применения порядок аргументов конфликтует с естественным порядком чтения. На этот случай в стандартной библиотеке есть функция flip, которая меняет местами два первых аргумента функции. Она позволяет «развернуть» функцию, когда зафиксировать хочется не первый, а второй параметр. Пользоваться ей стоит умеренно — обилие flip быстро запутывает, — но как инструмент она показывает, насколько гибко каррирование позволяет обращаться с функциями. В целом же привычка проектировать сигнатуры с прицелом на частичное применение отличает зрелый функциональный код: аргументы располагают так, чтобы самые частые способы использования функции выходили короткими и читаемыми.

Частые ошибки

  • Думать, что функция «требует все аргументы сразу». Не требует — недостающие можно дать позже.
  • Ставить скобки вокруг аргументов как в Python. В Haskell вызов — это просто f x y через пробел, без скобок.
  • Забывать про правоассоциативность стрелки. a -> b -> c — это a -> (b -> c), а не (a -> b) -> c.

Best practices

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

Итог. Функции объявляются как имя арг = тело, а под капотом каждая берёт один аргумент благодаря каррированию. Частичное применение — прямое следствие, и оно делает код компактным и переиспользуемым.

Проверьте себя
1. Что означает каррирование в Haskell?
AФункции готовят карри
BЛюбая функция берёт ровно один аргумент и возвращает функцию, ждущую следующий
CАргументы передаются в обратном порядке
DФункция кэширует свои результаты
2. Чему равно addTen, если addTen = add 10 и add x y = x + y?
AЧислу 10
BОшибке компиляции
CФункции Int -> Int, прибавляющей 10
DСписку [10]