Определение функций и карринг
В 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
- Пишите сигнатуры типов у функций верхнего уровня — это документация и страховка.
- Порядок аргументов выбирайте так, чтобы частичное применение было удобным: то, что меняется реже, ставьте раньше.
- Не бойтесь возвращать функции — это нормально и полезно.
Итог. Функции объявляются как имя арг = тело, а под капотом каждая берёт один аргумент благодаря каррированию. Частичное применение — прямое следствие, и оно делает код компактным и переиспользуемым.