Функции, каррирование и частичное применение
Понимаем, почему в OCaml функция нескольких аргументов на самом деле принимает их по одному, и какую силу это даёт.
Каррирование — представление функции многих аргументов как цепочки функций одного аргумента, каждая из которых возвращает следующую.
В OCaml функции — значения первого класса: их можно хранить, передавать и возвращать. Но есть тонкость, которая поначалу выглядит как причуда, а потом становится любимым инструментом: все функции каррированы. Функция «двух аргументов» на самом деле принимает первый и возвращает новую функцию, ждущую второй.
Определение функций
let add a b = a + b
(* тип: int -> int -> int *)
Прочитайте тип внимательно: int -> int -> int. Стрелка правоассоциативна, то есть это int -> (int -> int). Поэтому вызов add 2 3 — это на самом деле (add 2) 3: сперва add 2 возвращает функцию «прибавь 2», затем она применяется к 3.
Частичное применение
Каррирование напрямую даёт частичное применение: передаём не все аргументы и получаем специализированную функцию.
let add a b = a + b
let inc = add 1 (* int -> int: прибавляет 1 *)
let add10 = add 10
let r1 = inc 41 (* 42 *)
let r2 = add10 5 (* 15 *)
Это избавляет от шаблонного кода. Вместо let inc x = add 1 x достаточно let inc = add 1. Особенно полезно с функциями высшего порядка вроде List.map.
Анонимные функции fun
let squares = List.map (fun x -> x * x) [1; 2; 3; 4]
(* [1; 4; 9; 16] *)
Запись fun a b -> ... — тоже каррированная форма, сокращение для fun a -> fun b -> .... По сути let add a b = ... — это удобный синтаксис для let add = fun a b -> ....
Применение функций
Аргументы пишутся через пробел, без скобок и запятых: f x y, а не f(x, y). Скобки нужны лишь для группировки: f (g x) y. Если написать f g x y, это будет вызов f с тремя аргументами — частая ошибка.
Как работает под капотом
Может показаться, что каррирование создаёт промежуточную функцию на каждый аргумент. На практике компилятор оптимизирует «полные» применения: когда вы вызываете add 2 3 сразу со всеми аргументами, генерируется прямой вызов без промежуточного замыкания. Замыкание создаётся только при настоящем частичном применении (add 2). Так каррирование даёт выразительность бесплатно в типичном случае.
Частые ошибки
- Писать
f(x, y). Это вызовfс одним аргументом — кортежем(x, y). Каррированная функция ждётf x y. - Забыть скобки вокруг составного аргумента:
print_int succ 5— ошибка; нужноprint_int (succ 5). - Думать, что частичное применение «дозаполняется». Оно просто возвращает обычную функцию.
Итоги
- Все функции каррированы:
int -> int -> intчитается какint -> (int -> int). - Частичное применение — передать часть аргументов и получить специализированную функцию.
- Аргументы передаются через пробел:
f x y, скобки — только для группировки.