λ-calculus

I. Начало

Функции высшего порядка

Начнем с несколько отстраненной темы. В математике полно функций — алгебраических вроде exp, тригонометрических вроде sin и других. Функции можно задавать и руками, например:

f(x) = 2x + 1

Эта запись появилась еще в 18 веке, когда никакой речи об анализе функций с той точки зрения, с которой мы их рассмотрим, идти не могло. Все, что означает эта запись — это что

f(1) = 2*1 + 1
f(2) = 2*2 + 1
f(3) = 2*3 + 1
...

и так далее.

Но есть одно "но", которое перечеркивает все удобства. Проблема в том, что f(x) = 2x + 1 утверждает, что буква f, за которой следует открывающая скобка, затем какое-то выражение, затем закрывающая скобка, обозначает такое-то значение. О том, что означает сама буква f по отдельности — речи не идет, и даже из контекста это не всегда понятно.

Попробуйте как программист упростить следующую запись:

f(x) = sin(sin(sin(x))) + cos(cos(cos(x))) + tg(tg(tg(x))) + ctg(ctg(ctg(x)))

У этой задачи есть очевидное (для программиста), изящное и понятное решение:

rep(x, g) = g(g(g(x)))
f(x) = rep(x, sin) + rep(x, cos) + rep(x, tg) + rep(x, ctg)

Вот только неправильное. Потому что в алгебре сами по себе три буквы sin ничего не означают, тем более — если они подаются как параметр какой-то еще функции. Перефразируя: в классической математике нет способа воспользоваться функцией, кроме как сразу ее применить к какому-либо аргументу. Говоря формально: в классической математике нет функций высшего порядка.

Для того, чтобы названия функций имели смысл сами по себе, а не только при применении к какому-либо аргументу, вводят дополнительную нотацию. Было:

f(x) = 2x+1

Стало:

f := \x. 2x+1

Знак λ (лямбда) обозначает функцию. После него идет название аргумента, затем точка, затем — выражение, по которому функция вычисляется. Дальше с функциями можно работать, как в обычной математике:

double := \x. x*2
triple := \x. x*3
x_five := \x. (double(x)) + (triple(x))

Каррирование

Если мы попробуем теперь правильно расписать функцию "rep", нас ожидает сюрприз. λ-функция всегда принимает ровно один аргумент. А если нужно больше? Для этого мы используем трюк, называемый каррированием. Мы определим функцию, которая принимает первый аргумент и возвращает новую функцию, которая принимает второй аргумент и возвращает еще одну функцию, которая принимает третий аргумент, и так далее. Последняя функция примет последний аргумент и вернет собственно результат. Вот так, например, можно определить сложение:

add := \a. \b. a + b

Если, например, формально посчитать add 1, то получится \b. 1 + b — функция возвращает другую функцию. Теперь, если эту новую функцию опять применить к числу, скажем, два, получится 1 + 2, то есть 3.

(add(1))(2) = 3

Поскольку много скобок ставить неудобно, есть два упрощения. Во-первых, f x — это то же самое, что f(x). Во-вторых, поскольку каррирование используется очень часто, то f x y — это то же самое, что (f x) y. Упрощая:

add 1 2 = 3

Вот, кстати, как тот пример с rep записывается правильно:

rep := \x. \g. g(g(g x))
f := \x. (rep x sin) + (rep x cos) + (rep x tg) + (rep x ctg)

Вычисления

В λ-исчислении выражения составляются при помощи создания λ-функций и применения их друг к другу.

Давайте формально определим, что значит "вычислить выражение". Для этого можно проводить над выражением друг за другом следущие операции:

  1. α-эквивалентность. Это математический аналог переименования переменных. Например: \x. x*2+t можно заменить на \y. y*2+t.
  2. β-редукция. Это математический аналог вызова функции. Выражение вида (\x.E1)(E2) можно заменить на E1, в котором все вхождения x заменяются на E2. Например: (\x. 2*x+1)(1+2) можно заменить на 2*(1+2)+1.
  3. η-преобразование. Это утверждение о том, что функции задаются своими значениями во всех точках и не имеют внутренней структуры. Выражение вида \x.(E x) разрешается заменить на E (конечно, только если x не используется внутри E). Например: \x.(sin x) — это то же самое, что sin. К слову, почему это интуитивно очевидное правило нужно вводить отдельно: в некоторых областях математики для задания функции недостаточно ее значений во всех точках. Например, если мы исследуем полиномы по модулю именно как полиномы, а не как функции. Например, если вычисления проводятся по модулю 2, x^2 + x + 1 — это не то же самое, что 1.

Для вычисления значений выражений мы будем применять в каком-то порядке эти операции. Вообще говоря, в большинстве случаев достаточно β-редукции, но об этом чуть позже. Применять эти операции мы будем, пока не получится выражение, в котором нет вызовов функций. Такое выражение называется β-нормальной формой.

Арифметика

Теперь предлается немного расширить границы применимости λ-исчисления. В самом деле, у нас такая стройная теория, а 5 + 7 мы все еще считаем используя какую-то арифметику. Пора от этого избавляться.

Итак, отныне каждый раз, когда в коде будут использоваться числа, мы будем интерпретировать их как специальные λ-функции, а именно:

0 := \f. \x. x
1 := \f. \x. f x
2 := \f. \x. f(f x)
3 := \f. \x. f(f(f x)
4 := \f. \x. f(f(f(f x))
...

Словом, число n — это λ-функция, которая принимает два аргумента (ну, через каррирование): f и x, а затем n раз применяет f к x. Например: 3 g y = g(g(g y))), а 3 g = \x. g(g(g x)) (так сказать, утроение функции).

Теперь мы можем реализовать, например, инкремент (succ — сокращение от successor, следующий):

succ := \n. \f. \x. n f (f x)

Иными словами, succ принимает число n, и возвращает нечто. Когда это нечто вызывается от f и x, оно не сразу передает эти два аргумента числу n, а сначала лишний раз применяет f к x — вот и получается на один больше. Если вы любите формальности, вот вам пример с вычислением β-редукциями:

succ 3 ->
-> \f. \x. 3 f (f x) ->
-> \f. \x. (\g. \y. g(g(g y))) f (f x) ->
-> \f. \x. (\y. f(f(f y))) (f x) ->
-> \f. \x. f(f(f(f x))) = 4

Теперь можно реализовать и сложение. Сложение a и b — это значит a раз применить succ к b. Записывается это на удивление просто:

add := \a. \b. a succ b = \a. a succ

Дальше по списку идет умножение. Здесь проще воспользоваться пониманием числа как умножителя функции. Произведение a на b — это нечто, принимающее функцию и повторяющее ее a*b раз. Для этого достаточно ее сначала повторить b раз, затем результат — a раз:

mul := \a. \b. \f. a (b f)

Например:

mul 2 3 ->
-> \f. 2 (3 f) ->
-> \f. 2 ((\g. \y. g(g(g y))) f) ->
-> \f. 2 (\y. f(f(f y))) ->
-> \f. (\g. \x. g(g x)) (\y. f(f(f y))) ->
-> \f. \x. (\y. f(f(f y))) ((\y. f(f(f y))) x) ->
-> \f. \x. (\y. f(f(f y))) (f(f(f x))) ->
-> \f. \x. f(f(f(f(f(f x))))) = 6

Чтобы вы почувствовали всю красоту λ-исчисления, реализуем еще возведение в степень pow a b. pow a b — это то же самое, что \f. a (a (... (a f))), где a повторяется b раз. Погодите-ка, повторение функции — это же ровно то, как числа и определены!

pow := \a. \b. b a

Несколько сложнее ввести декремент и вычитание: проблема в том, что понять, что значит "применить функцию -1 раз" — не самое простое дело. К этой задаче мы вернёмся чуть позже. А в следующей части займёмся логикой.