Функции, каррирование и частичное применение

Понимаем, почему в 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, скобки — только для группировки.
Проверьте себя
1. Что означает тип `int -> int -> int`?
AФункция, принимающая список из трёх int
BФункция, которая берёт int и возвращает функцию int -> int
CТри отдельные функции
DКортеж из трёх int
2. Что делает `let inc = add 1` при `let add a b = a + b`?
AСразу вычисляет add 1 как ошибку
BСоздаёт функцию частичным применением: прибавляет 1
CОбъявляет переменную 1
DНичего, нужны оба аргумента
3. Что на самом деле означает вызов `f(x, y)` в OCaml?
AВызов f с двумя аргументами
BВызов f с одним аргументом — кортежем (x, y)
CСинтаксическую ошибку
DТо же, что f x y