Лямбда исчисление
Реферат, 18 Марта 2012, автор: пользователь скрыл имя
Описание работы
Функциональные языки программирования появились в качестве средства для написания программ, не содержащих в явном виде понятий ячеек памяти для хранения значений (переменных) и последовательности вычислений как процесса изменения состояния памяти. Тем не менее, основа для создания таких языков была предложена еще в середине 30-х годов 20-го века Алонзо Черчем (Alonzo Church) и Стефаном Клини (Stephen Kleene). Мы рассмотрим теорию Черча, названную им лямбда-исчислением, в качестве теоретической основы и "минимального" функционального языка программирования.
Работа содержит 1 файл
Лямбда исчисление.docx
— 147.45 Кб (Скачать)Лямбда-исчисление —
лишь одно из нескольких фундаментальных
исчислений, используемых для подобных
целей. Пи-исчисление (pi-
Лямбда-исчисление можно расширить и обогатить несколькими способами. Во-первых, часто для удобства добавляют особый синтаксис для чисел, кортежей, записей и т. п., чье поведение, в принципе, можно смоделировать и в базовом языке. Интереснее добавить более сложные возможности, такие как изменяемые ссылочные ячейки или нелокальная обработка исключений. Эти свойства можно смоделировать на базовом языке только путем достаточно тяжеловесного перевода. Такие расширения, в конце концов, приводят к языкам вроде ML (Gordon, Milner, and Wadsworth, 1979, Milner, Tofte, and Harper, 1990, Weis, Aponte, Laville, Mauny, and Suárez, 1989, Milner, Tofte, Harper, and MacQueen, 1997), Haskell (Hudak et al., 1992) или Scheme (Sussman and Steele, 1975, Kelsey, Clinger, and Rees, 1998). Как мы увидим в последующих главах, расширения базового языка часто требуют расширения системы типов.
5.1 Основы
Процедурная (или функциональная) абстракция — ключевое свойство почти всех языков программирования. Вместо того, чтобы выписывать одно и то же вычисление раз за разом, мы пишем процедуру или функцию, которая проделывает это вычисление в общем виде, используя один или несколько именованных параметров, а затем при необходимости вызываем эту процедуру, каждый раз задавая значения параметров. К примеру, для программиста естественно взять длинное вычисление с повторяющимися частями, вроде
и переписать его в виде factorial(5) + factorial(7) - factorial(3), где
factorial(n) = if n=0 then 1 else n * factorial(n-1)
Для каждого неотрицательного числа n подстановка аргумента n в функцию factorial дает в результате факториал n. Используя нотацию <<λn. ...>> обозначающую <<функцию, которая для каждого n, дает …>>, определение factorial можно переформулировать как
factorial = λn. if n=0 then 1 else n * factorial(n-1)
Теперь factorial(0) означает <<функция λn. if n=0 then 1 else ..., примененная к аргументу 0>>, то есть, <<значение, которое получается, если аргумент n в теле функции (λn. if n=0 then 1 else ...) заменить на 0>>, то есть, <<if 0=0 then 1 else ...>>, то есть, 1.
Лямбда-исчисление (lambda-
Синтаксис
лямбда-исчисления состоит из трех
видов термов.1 Переменная x са
t ::= |
термы: | |
x |
переменная | |
λx. t |
абстракция | |
t t |
применение |
В следующих подразделах это
Абстрактный и конкретный синтаксис
При
обсуждении синтаксиса языков программирования
полезно различать два уровня2
Преобразование
из конкретного в абстрактный
синтаксис происходит в два этапа.
Сначала лексический анализатор (lexical
analyzer) (или лексер, lexer) переводит последовательность символов,
написанных программистом, в последовательность лексем (
[.+ 1 [.* 2 3 ] ] [.* [.+ 1 2 ] 3 ] |
В этой книге мы обращаем основное внимание на абстрактный, а не конкретный синтаксис. Грамматики, вроде той, что мы привели для лямбда-термов, следует рассматривать как описания разрешенных видов деревьев, а не последовательностей лексем или символов. Разумеется, когда мы будем записывать термы в примерах, определениях, теоремах и доказательствах, нам придется выражать их с помощью конкретной, линейной записи, но мы всегда имеем в виду соответствующие абстрактные синтаксические деревья.
Чтобы избежать излишних скобок, для записи лямбда-термов в линейной форме мы следуем двум соглашениям. Во-первых, применение функции лево-ассоциативно — то есть, s t u обозначает то же дерево, что (s t) u:
[.apply [.apply s t ] u ] |
Во-вторых, тела абстракций простираются направо как можно дальше, так что, например, λx. λy. x y x означает то же самое, что и λx. (λy. ((x y) x)):
[.λx [.λy [.apply [.apply x y |
Переменные и метапеременные
Еще одна тонкость в приведенном определении синтаксиса касается использования метапеременных. Мы будем продолжать использовать метапеременную t (а также s и u, с нижними индексами и без них), обозначающую произвольный терм.3 Аналогично, x (а также y и z) замещает произвольную переменную. Заметим, что здесь x — это метапеременная, значениями которой являются другие переменные! К сожалению, число коротких имен ограничено, и нам потребуется иногда использовать x, y и т. д. для обозначения переменных объектного языка. В таких случаях, однако, из контекста всегда будет ясно, что имеется в виду. Например, в предложении <<Терм λx. λy. x y имеет вид λz. s, где z = x, а s = λy. x y>> имеем z и s — имена метапеременных, а x и y — имена переменных объектного языка.
Область видимости
Последнее, что нам требуется разъяснить в синтаксисе лямбда-исчисления, — область видимости (scope) переменных.
Переменная x называется связан
Терм
без свободных переменных называется замкнутым (closed);
замкнутые термы называют также комбинаторами (
id = λx.x;
не выполняет никаких действий, а просто возвращает свой аргумент.
Операционная семантика
В своей чистой форме лямбда-исчисление не содержит встроенных констант и элементарных операторов — ни чисел, ни арифметических операций, ни условных выражений, ни записей, ни циклов, ни последовательного выполнения выражений, ни ввода-вывода, и т. д. Единственное средство для <<вычисления>> термов — применение функций к аргументам (которые сами являются функциями). Каждый шаг вычисления состоит в том, что в терме-применении, в котором левый член является абстракцией, связанная переменная в теле этой абстракции заменяется на правый член. Записывается это так:
(λx.t12) t2 → [x ↦ t2] t12 |
где
[x ↦ t2] t12 означает
<<терм, получаемый из t12 путем
замены всех свободных вхождений x на t2>>.
Например, терм (λx.
x) y за один шаг вычисления переходит в y,
а терм (λx. x (λx.
x)) (u r) переходит в u
r (λx. x). Вслед за Чёрчем, терм вида (λx.
t12) t2 называется редексом (redex) (reducible expression (redex), <<сокращаемое
выражение>>), а операция переписывания
редекса в соответствии с указанным правилом
называется бета-редукцией (
В течение многих лет разработчики и теоретики языков программирования изучали различные стратегии вычисления в лямбда-исчислении. Каждая стратегия определяет, какие редексы в терме могут сработать на следующем шаге вычисления.4
- При полной бета-редукции (full beta-reduction) в любой момент может сработать любой редекс. На каждом шаге мы выбираем какой-нибудь редекс где-то внутри вычисляемого терма, и проводим шаг редукции. Рассмотрим, например, терм
(λx.x) ((λx.x) (λz. (λx.x) z))
который можно записать для удобства чтения в виде id (id (λz. id z)). В этом терме содержится три редекса:
id (id (λz. id z))
id (id (λz. id z))
id (id (λz. id z))
При полной бета-редукции можно, например, начать с самого внутреннего редекса, затем обработать промежуточный, а затем — внешний:
id (id (λz. id z)) | |
→ |
id (id (λz.z)) |
→ |
id (λz.z) |
→ |
λz.z |
¬→ |
- При стратегии нормального порядка вычислений (normal order) всегда сначала сокращается самый левый, самый внешний редекс. При такой стратегии указанный терм обрабатывался бы так:
id (id (λz. id z)) | |
→ |
id (λz. id z) |
→ |
λz. id z |
→ |
λz.z |
¬→ |
-
При такой стратегии (а также всех перечисленных ниже) отношение вычисления на самом деле является частичной функцией: каждый терм t за шаг переходит не более чем в один терм t′. - Стратегия вызова по имени (call by name) еще более строга: она не позволяет проводить редукцию внутри абстракций. Начиная с того же самого терма, первые две редукции мы проведем так же, как и при нормальном порядке вычислений, но потом остановимся и будем считать λz. id z нормальной формой:
id (id (λz. id z)) | |
→ |
id (λz. id z) |
→ |
λz. id z |
¬→ |
-
Варианты вызова по имени использовались в некоторых хорошо известных языках, в том числе в Алголе-60 (Naur et al., 1963) и Haskell (Hudak et al., 1992). В Haskell, на самом деле, используется оптимизированная версия, известная как вызов по необходимости (call by need) (Wadsworth, 1971; Ariola et al., 1995), в которой вместо того, чтобы перевычислять аргумент при каждом использовании, при первом вычислении все вхождения аргумента заменяются значением, и таким образом пропадает необходимость вычислять его заново в следующий раз. При такой стратегии требуется во время выполнения совместно использовать структуры данных между представлениями термов — в сущности, получается отношение редукции на графах (graphs) абстрактного синтаксиса, а не на синтаксических деревьях. - В большинстве языков используется стратегия вызова по значению (call by value). В соответствии с ней, сокращаются только самые внешние редексы, и, кроме того, редекс срабатывает только в том случае, если его правая часть уже сведена к значению (value) — замкнутому терму, который уже вычислен и не может быть редуцирован далее.5 При такой стратегии наш пример будет редуцироваться так:
id (id (λz. id z)) | |
→ |
id (λz. id z) |
→ |
λz. id z |
¬→ |
-
Стратегия вызова по значению строга в том смысле, что аргументы функции всегда вычисляются, независимо от того, используются они в теле функции или нет. С другой стороны, нестрогие(non-strict) (или ленивые, lazy) стратегии вычисления — вызов по имени или по необходимости, — вычисляют только те аргументы, которые действительно используются.
Выбор
стратегии вычисления почти ни на
что не влияет в контексте обсуждения
систем типов. Вопросы, которые ведут
к использованию тех или иных
свойств типов, и методы, используемые
для ответа на эти вопросы, для
всех стратегий практически
5.2 Программирование на языке лямбда-исчисления
Лямбда-исчисление — значительно более мощный формализм, чем кажется при первом взгляде на его крошечное определение. В этом разделе мы продемонстрируем несколько стандартных примеров программирования в рамках этого формализма. Эти примеры совершенно не означают, что лямбда-исчисление следует считать полноценным языком программирования — во всех распространенных языках те же самые задачи можно решить более понятным и эффективным образом. Скорее, это некоторая разминка, чтобы дать читателю почувствовать, как устроена эта система.