Автор: Пользователь скрыл имя, 18 Марта 2012 в 20:08, реферат
Функциональные языки программирования появились в качестве средства для написания программ, не содержащих в явном виде понятий ячеек памяти для хранения значений (переменных) и последовательности вычислений как процесса изменения состояния памяти. Тем не менее, основа для создания таких языков была предложена еще в середине 30-х годов 20-го века Алонзо Черчем (Alonzo Church) и Стефаном Клини (Stephen Kleene). Мы рассмотрим теорию Черча, названную им лямбда-исчислением, в качестве теоретической основы и "минимального" функционального языка программирования.
Лямбда-исчисление —
лишь одно из нескольких фундаментальных
исчислений, используемых для подобных
целей. Пи-исчисление (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). Как мы увидим в последующих главах, расширения базового языка часто требуют расширения системы типов.
Процедурная (или функциональная) абстракция — ключевое свойство почти всех языков программирования. Вместо того, чтобы выписывать одно и то же вычисление раз за разом, мы пишем процедуру или функцию, которая проделывает это вычисление в общем виде, используя один или несколько именованных параметров, а затем при необходимости вызываем эту процедуру, каждый раз задавая значения параметров. К примеру, для программиста естественно взять длинное вычисление с повторяющимися частями, вроде
и переписать его в виде 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
(λ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 |
¬→ |
id (id (λz. id z)) | |
→ |
id (λz. id z) |
→ |
λz. id z |
→ |
λz.z |
¬→ |
id (id (λz. id z)) | |
→ |
id (λz. id z) |
→ |
λz. id z |
¬→ |
id (id (λz. id z)) | |
→ |
id (λz. id z) |
→ |
λz. id z |
¬→ |
Выбор
стратегии вычисления почти ни на
что не влияет в контексте обсуждения
систем типов. Вопросы, которые ведут
к использованию тех или иных
свойств типов, и методы, используемые
для ответа на эти вопросы, для
всех стратегий практически
Лямбда-исчисление — значительно более мощный формализм, чем кажется при первом взгляде на его крошечное определение. В этом разделе мы продемонстрируем несколько стандартных примеров программирования в рамках этого формализма. Эти примеры совершенно не означают, что лямбда-исчисление следует считать полноценным языком программирования — во всех распространенных языках те же самые задачи можно решить более понятным и эффективным образом. Скорее, это некоторая разминка, чтобы дать читателю почувствовать, как устроена эта система.