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

Функции в F# — полноценные значения, и все они каррированы, что открывает частичное применение.

Каррирование — представление функции нескольких аргументов как цепочки функций одного аргумента; add a b на деле — функция от a, возвращающая функцию от b.

Функции — это значения

В F# функцию можно положить в let, передать аргументом, вернуть из другой функции. Объявления значения и функции выглядят одинаково — через let.

let square x = x * x
let apply f v = f v        // f — функция-аргумент
printfn "%d" (apply square 7)

Вывод:

49

Каррирование по умолчанию

Объявление let add a b = a + b имеет тип int -> int -> int. Стрелки читаются справа налево: это функция от a, которая возвращает функцию от b, которая возвращает int. Поэтому вызов с одним аргументом легален.

let add a b = a + b
let add10 = add 10        // частичное применение
printfn "%d" (add10 5)
printfn "%d" (add10 100)

Вывод:

15
110

add 10 не вычисляет сумму, а возвращает новую функцию add10, которая ждёт второй аргумент. Это и есть частичное применение.

Зачем частичное применение

Оно позволяет создавать специализированные функции из общих, не повторяя код. Очень удобно при передаче функций в map, filter и подобные.

let multiply factor x = factor * x
let double = multiply 2
let triple = multiply 3
let xs = [1; 2; 3]
printfn "%A" (List.map double xs)
printfn "%A" (List.map triple xs)

Вывод:

[2; 4; 6]
[3; 6; 9]

Анонимные функции (лямбды)

Безымянную функцию задают через fun.

let nums = [1; 2; 3; 4]
let evens = List.filter (fun n -> n % 2 = 0) nums
printfn "%A" evens

Вывод:

[2; 4]

Как работает под капотом

Каждая функция нескольких аргументов компилируется как замыкание: применение первого аргумента создаёт объект, захватывающий это значение и реализующий вызов оставшихся аргументов. Для производительности компилятор оптимизирует «полные» вызовы (со всеми аргументами сразу) в прямой вызов без промежуточных замыканий, так что каррирование почти ничего не стоит, когда вы передаёте все аргументы.

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

  • Считать, что вызов с неполным числом аргументов — ошибка. Это частичное применение.
  • Путать порядок аргументов: для частичного применения «настроечный» параметр ставят первым.
  • Ждать вычисления при add 10 — получаете функцию, а не число.

Итоги

  • Функции — значения: их хранят, передают и возвращают.
  • В F# функции каррированы: a -> b -> c — цепочка функций одного аргумента.
  • Частичное применение создаёт специализированные функции из общих.
  • Лямбды объявляют через fun x -> ....
Проверьте себя
1. Что такое каррирование?
AОптимизация циклов
BПредставление функции нескольких аргументов как цепочки функций одного аргумента
CСпособ кэширования результатов
DПерегрузка операторов
2. Что возвращает add 10, если let add a b = a + b?
AЧисло 10
BОшибку компиляции
CНовую функцию, ожидающую второй аргумент
DЗначение null
3. Как объявить анонимную функцию (лямбду)?
Alambda x: x
Bfun x -> ...
C=> x
Dfunc(x)