Бенджамин Пирс
18 сентября 2008 г.
2
Предисловие
Предполагаемая аудитория
Книга предназначена для двух основных групп читателей: с одной стороны,
для аспирантов и исследователей, специализирующихся на языках програм-
мирования и теории типов; с другой стороны, для аспирантов и продвинутых
студентов из других областей информатики, которые хотели бы получить вве-
дение в основные понятия теории языков программирования. Для первой из
этих групп книга дает обширный обзор рассматриваемой области, с глуби-
ной, достаточной для чтения исследовательской литературы. Для второй она
предоставляет подробный вводный материал и обширное собрание примеров,
упражнений и практических ситуаций. Книга может служить как для ввод-
ных курсов аспирантского уровня, так и для более продвинутых семинаров по
языкам программирования.
Цели
Основной моей задачей был охват материала, включая базовую операцион-
ную семантику и связанные с ней способы построения доказательств, бестипо-
вое лямбда-исчисление, простые системы типов, универсальный и экзистенцио-
нальный полиморфизм, реконструкцию типов, иерархии типов, ограниченную
квантификацию, рекурсивные типы и операторы над типами, а также краткое
обсуждение некоторых других тем.
Второй задачей был прагматизм. Книга сосредоточена на использовании
систем типов в языках программирования, за счет некоторых тем (например,
денотационной семантики), которые, вероятно, были бы включены в более ма-
тематически ориентированный текст по типизированным лямбда-исчислениям.
В качестве базового вычислительного формализма выбрано лямбда-исчисление
с вызовом по значению, которое соответствует большинству современных язы-
ков программирования и легко расширяется императивными конструкциями
3
4
Структура книги
не вдаюсь; эти связи важны, но они увели бы нас слишком далеко в сторону.
Многие продвинутые черты языков программирования и систем типов лишь
кратко упомянуты, например, зависимые типы, типы-пересечения, а также
соответствие Карри-Ховарда; краткие разделы по этим вопросам служат как
отправные пункты для дальнейшего чтения. Наконец, за исключением крат-
кого экскурса в Java-подобный базовый язык (Глава ??), в книге рассматри-
ваются исключительно языки на основе лямбда-исчисления; однако понятия
и механизмы, исследованные в этих рамках, могут напрямую быть перенесе-
ны в родственные области, такие как типизированные параллельные языки,
типизированные ассемблеры и специализированные исчисления объектов.
Требуемая подготовка
Текст не предполагает никаких знаний по теории языков программирования,
однако читатели должны в известной степени обладать математической зре-
лостью — а именно, иметь опыт строгой учебной работы в таких областях, как
дискретная математика, алгоритмы и элементарная логика.
Читатели должны быть знакомы по крайней мере с одним функциональ-
ным языком высокого уровня (Scheme, ML, Haskell и т. п.) и с основными
понятиями в теории языков программирования и компиляторов (абстракт-
ный синтаксис, грамматики BNF, вычисление, абстрактные машины и т. п.).
Этот материал доступен для изучения во множестве замечательных учебни-
ков; я особенно ценю “Основные понятия языков программирования” Фридма-
на, Ванда и Хейнса (Friedman, Wand, and Haynes, Essentials of Programming
Languages, 2001), а также “Прагматику языков программирования” Скотта
(Scott, Programming Language Pragmatics, 1999). В нескольких главах полезен
будет опыт работы с объектно-ориентированным языком, например, с Java
(Arnold and Gosling, 1996).
Главы, посвященные конкретным реализациям программ проверки типов,
содержат большие фрагменты кода на OCaml (он же Objective Caml), попу-
лярном диалекте языка ML. Для чтения этих глав будет полезно знать OCaml
заранее, но это не абсолютно необходимо; используется только небольшое под-
множество языка, и его конструкции объясняются при первом использовании.
Эти главы образуют отдельную нить в ткани книги, и при желании их можно
полностью пропустить.
В настоящее время лучший учебник по OCaml — Кузино и Мони (Cousineau
and Mauny, 1998). Кроме того, вполне читабельны и учебные материалы, по-
ставляемые с дистрибутивом OCaml (http://caml.inria.fr или http://www.
ocaml.org).
Для читателей, знакомых с другим крупным диалектом ML, Стандартным
ML, фрагменты кода на OCaml не должны представлять трудности. Срели
популярных учебников по Стандартному ML можно назвать книги Поульсона
(Paulson 1996) и Ульмана (Ullman 1997).
Книгу можно также использовать как основной текст в более широко орга-
низованном аспирантском курсе по теории языков программирования. Такой
курс может потратить половину или две трети семестра на прохождение боль-
шей части книги, а остаток времени посвятить, скажем, рассмотрению теории
параллелизма на основе книги Милнера по пи-исчислению (Milner 1999), вве-
дению в логику Хоара и аксиоматическую семантику (напр., Winskel 1993) или
обзору продвинутых языковых конструкций вроде продолжений или модуль-
ных систем.
Если существенная роль отводится курсовым проектам, может оказаться
полезным отложить часть теоретического материала (скажем, нормализацию,
и, возможно, некоторые главы по метатеории), чтобы продемонстрировать ши-
рокий спектр примеров, прежде чем студенты выберут себе курсовые.
Упражнения
Типографские соглашения
Электронные ресурсы
Веб-сайт, посвященный этой книге, можно найти по адресу
http://www.cis.upenn.edu/~bcpierce/tapl
Благодарности
Читатели, которые найдут эту книгу полезной, должны прежде всего быть
благодарны четырем учителям — Луке Карделли, Бобу Харперу, Робину Мил-
неру и Джону Рейнольдсу, — которые научили меня почти всему, что я знаю
о языках программирования и типах.
Остаток своих знаний я приобрел в основном в совместных проектах; поми-
мо Луки, Боба, Робина и Джона, среди моих партнеров в этих проектах были
Мартин Абади, Гордон Плоткин, Рэнди Поллак, Дэвид Н. Тёрнер, Дидье Ре-
ми, Давиде Санджорджи, Адриана Компаньони, Мартин Хофман, Джузеппе
Кастанья, Мартин Стеффен, Ким Брюс, Наоки Кобаяси, Харуо Хосоя, Ацуси
Игараси, Филип Уодлер, Питер Бьюнеман, Владимир Гапеев, Майкл Левин,
Питер Сьюэлл, Джером Вуийон и Эйдзиро Сумии. Совместная работа послу-
жила для меня не только источником понимания, но и удовольствия от работы
над этой темой.
10
Доказательства программ
настолько скучны, что
социальные механизмы
математики на них не работают.
Введение
11
12 Глава 1. Введение
1.2.2 Абстракция
Еще один вид пользы, которую системы типов приносят в процессе разработки
программ — это поддержание дисциплины программирования. В частности, в
контесксте построения крупномасштабных программных систем, системы ти-
пов являются стержнем языков описания модулей, при помощи которых упа-
ковываются и связываются компоненты больших систем. Типы появляются в
интерфейсах модулей (или близких по смыслу структур, таких, как классы);
в сущности, сам интерфейс можно рассматривать как «тип модуля», содержа-
щий информацию о возможностях, которые модуль предоставляет — своего
рода частичный контракт между разработчиками и пользователями.
Разбиение больших систем на модули с ясно определенными интерфейсами
приводит к более абстрактному стилю проектирования, когда интерфейсы раз-
рабатываются и обсуждаются отдельно от их окончательной реализации. Как
правило, более абстрактный стиль мышления повышает качество проектов.
1.2.3 Документация
Типы полезны также при чтении программ. Объявления типов в заголовках
процедур и интерфейсах модулей представляют собой разновидность докумен-
тации и дают ценные указания о поведении программы. Более того, в отличие
от описаний, упрятанных в комментарии, такая документация не может уста-
реть, поскольку она проверяется при каждом прогоне компилятора. Эта роль
типов особенно существенна в сигнатурах модулей.
вании систем типов. В принципе, необходимые для этого механизмы (на основе зависимых
типов — см. $??) хорошо изучены, однако такое их использование, где соблюдался бы ба-
ланс между выразительной силой, предсказуемостью и разрешимостью проверки типов, и
сложностью программных аннотаций, остается тяжелой задачей. О некоторых недавних до-
стижениях в этой области можно прочитать у Си и Пфеннинга (Xi and Pfenning 1998, 1999).
18 Глава 1. Введение
1.2.5 Эффективность
Первые системы типов в информатике, начиная с 50-х годов в таких языках,
как Фортран (Backus 1981), были введены для того, чтобы улучшить эффек-
тивность вычислений путем различения арифметических выражений с целым
значением и с вещественным значением; это позволяло компилятору использо-
вать различные представления чисел и порождать соответствующие машин-
ные команды для элементарных операций. В безопасных языках можно до-
стичь большей эффективности, избегая множества динамических проверок,
которые иначе потребовались бы для обеспечения безопасности (можно ста-
тически доказать, что проверка всегда даст положительный результат). В на-
ше время большинство высокопроизводительных компиляторов существенным
ообразом опираются при оптимизации и порождении кода на информацию,
собранную процедурой проверки типов. Даже компиляторы для языков без
типовых систем как таковых проделывают большую работу, чтобы получить
приближения к этой информации о типах.
Выигрыш в эффективности на основе информации о типах может возник-
нуть в неожиданных местах. Например, недавно было показано, что не только
решения о генерации кода, но и представление указателей в параллельных
научных вычислениях может быть улучшено с помощью информации, порож-
даемой при проверке типов. Язык Titanium (Yelick et al. 1998) использует вы-
вод типов для анализа области видимости указателей и способен принимать
измеримо лучшие решения на основе этих данных, чем программисты, опти-
мизирующие программы вручную. Компилятор ML Kit с помощью мощного
алгоритма вывода регионов (Gifford, Jouvelot and Sheldon 1987; Jouvelot and
Gifford 1991; Talpin and Jouvelot 1992; Tofte and talpin 1994, 1997; Tofte and
Birkedal 1998) заменяет большинство вызовов сборщика мусора (в некоторых
программах все вызовы) стековыми операциями управления памятью.
Математический аппарат
25
26 Глава 2. Математический аппарат
2.3 Последовательности
Определение 2.3.1 Последовательность записывается путем перечисления эле-
ментов через запятую. Мы используем запятую как для добавления элемен-
та к последовательности в начало (вроде cons в Лиспе) или конец, так и
для скленивания двух последовательностей (вроде append в Лиспе). Напри-
мер, если a обозначает последовательность 3, 2, 1, а b — последовательность
5, 6, то 0, a обозначает последовательность 0, 3, 2, 1, a, 0 обозначает 3, 2, 1, 0,
а b, a обозначает 5, 6, 2, 3, 1. (Использование запятой для двух разных опе-
раций — аналогов cons и append, — не приводит к путанице, если нам не
требуется говорить о последовательностях последовательностей.) После-
довательность чисел от 1 до n обозначается 1..n (через две точки). Запись
|a| означает длину последовательности a. Пустая последовательность обо-
значается знаком • или пробелом. Одна последовательность называется пе-
рестановкой другой, если они содержат одни и те же элементы, возможно,
в разном порядке.
2.4 Индукция
Доказательства по индукции встречаются в теории языков программирования
очень часто, как и в большинстве разделов информатики. Многие из этих
2.5. Справочная литература 29
Если P (0)
и для любого i, из P (i) следует P (i + 1),
то P (n) выполняется для всех n.
Доказательство — это
воспроизводимый опыт в деле
убеждения.
Джим Хорнинг
Часть I
Бестиповые системы
31
Глава 3
Бестиповые арифметические
выражения
3.1 Введение
В языке этой главы имеется лишь несколько синтаксических конструкций: бу-
левские константы true (истина) и false (ложь), условные выражения, чис-
ловая константа 0, арифметические операции succ (следующее число) и pred
(предыдущее число) и операция-тест iszero, которая возвращает true, будучи
применена к 0, и false, когда применяется к любому другому числу. Эти кон-
струкции можно компактно описать следующей грамматикой.
В этой главе изучается бестиповое исчисление логических значений и чисел (Рис. 3.2 на
стр. 49). Соответствующая реализация на OCaml хранится в веб-репозитории под именем
arith и описывается в Главе 4. Указания, как скачать и построить программу проверки,
находятся по адресу http://www.cis.upenn.edu/~bcpierce/tapl.
33
34 Глава 3. Бестиповые арифметические выражения
t ::= термы:
true константа «истина»
false константа «ложь»
if t then t else t условное выражение
0 константа «ноль»
succ t следующее число
pred t предыдущее число
iszero t проверка на ноль
Правила, которыми мы пользуемся при записи этой грамматики (и при запи-
си грамматик на протяжении всей этой книги) близки к стандартной форме
Бэкуса-Наура (см. Aho, Sethi and Ullman, 1986). В первой строке (t ::=) объ-
является, что мы определяем множество термов, и что мы будем употреблять
букву t для обозначения произвольного терма. Всюду, где встречается символ
t, можно подставить любой терм. Курсив справа — просто комментарии.
Символ t в правых частях правил грамматики называется метаперемен-
ной. Это переменная в том смысле, что t служит в качестве заместителя
какого-нибудь конкретного терма, но это «мета»-переменная, поскольку она
является переменной не объектного языка — простого языка программиро-
вания, чей синтаксис мы сейчас описываем, — а метаязыка, — знаковой си-
стемы, которая служит для описания. (На самом деле, в нашем теперешнем
объектном языке даже нет переменных; мы их введем в Главе 5.) Пристав-
ка meta- происходит из мета-математики, отрасли логики, которая изучает
математические свойства систем, предназначенных для математических и ло-
гических рассуждений (в частности, языков программирования). Оттуда же
происходит термин метатеория; он означает совокупность истинных утвер-
ждений, которые мы можем сделать о какой-либо логической системе (или
о языке программирования), а в расширенном смысле — исследование таких
утверждений. Таким образом, выражения вроде «метатеория наследования» в
этой книге могут пониматься как «формальное исследование свойств систем с
наследованием».
На протяжнии этой книги мы будем использовать метапеременную t, а
также близкие к ней буквы, скажем, s, u и r, и варианты вроде t1 или s0 , для
термов объектного языка, которым мы занимаемся в данный момент; далее
будут введены и другие буквы для обозначения выражений других синтакси-
ческих категорий. Полный список соглашений по использованию метаперемен-
ных можно найти в Приложении ??.
Пока что слова терм и выражение обозначают одно и то же. Начиная с
Главы ??, когда мы станем изучать исчисления, обладающие более богатым на-
бором синтаксических категорий (таких, как типы), словом выражение мы бу-
дем называть любые синтаксические объекты (в том числе выражения-термы,
выражения-типы, выражения-виды и т. п.), а терм будет употребляться в бо-
лее узком смысле, для конструкций, которые обозначают вычисления (т. е.,
выражения, которые можно подставить вместо метапеременной t).
Программа на нашем нынешнем языке — это всего лишь терм, построен-
ный при помощи перечисленных выше конструкций. Вот несколько примеров
программ, вместе с результатами их вычисления. Для краткости мы использу-
ем стандартные арабские цифры для записи чисел, которые формально пред-
3.2. Синтаксис 35
3.2 Синтаксис
Есть несколько эквивалентных способов определить синтаксис нашего языка.
Один из них мы уже видели — это грамматика на странице 34. Грамматика эта,
в сущности, всего лишь сокращенная форма записи следующего индуктивного
определения:
1. {true, false, 0} ⊆ T ;
t1 ∈ T t1 ∈ T t1 ∈ T
succ t1 ∈ T pred t1 ∈ T iszero t1 ∈ T
t1 ∈ T t2 ∈ T t3 ∈ T
if t1 then t2 else t3 ∈ T
S0 = ∅
Si+1 = {true, false, 0}
∪ {succ t1 , pred t1 , iszero t1 | t1 ∈ Si }
∪ {if t1 then t2 else t3 | t1 , t2 , t3 ∈ Si }.
Наконец, пусть [
S= Si .
i
Утверждение 3.2.6 T = S.
Доказательство: T определяется как наименьшее множество, удовлетворя-
ющее некоторым условиям. Таким образом, достаточно показать, что (а)
эти условия выполняются на S; и (б) что любое множество, удовлетворя-
ющее условиям, содержит S как подмножество (т. е, что S — наименьшее
множество, удовлетворяющее условиям).
В части (а) нам требуется проверить, что все три условия из Опреде-
ления 3.2.1 выполняются на S. Во-первых, поскольку S1 = {true, false, 0},
38 Глава 3. Бестиповые арифметические выражения
Consts(true) = {true}
Consts(false) = {false}
Consts(0) = {0}
Consts(succ t1 ) = Consts(t1 )
Consts(pred t1 ) = Consts(t1 )
Consts(iszero t1 ) = Consts(t1 )
Consts(if t1 then t2 else t3 ) = Consts(t1 ) ∪ Consts(t2 ) ∪ Consts(t3 )
size(true) = 1
size(false) = 1
size(0) = 1
size(succ t1 ) = size(t1 ) + 1
size(pred t1 ) = size(t1 ) + 1
size(iszero t1 ) = size(t1 ) + 1
size(if t1 then t2 else t3 ) = size(t1 ) + size(t2 ) + size(t3 ) + 1
depth(true) = 1
depth(false) = 1
depth(0) = 1
depth(succ t1 ) = depth(t1 ) + 1
depth(pred t1 ) = depth(t1 ) + 1
depth(iszero t1 ) = depth(t1 ) + 1
depth(if t1 then t2 else t3 ) = max(depth(t1 ), depth(t2 ), depth(t3 )) + 1
Вариант: t – константа.
Свойство выполняется непосредственно: |Consts(t)| = |{t}| = 1 = size(t).
Вариант: t = succ t1 , pred t1 или iszero t1
Согласно индуктивному предположению, |Consts(t1 )| ≤ size(t1 ). Рассуждаем
так: |Consts(t)| = |Consts(t1 )| ≤ size(t1 ) < size(t).
Вариант: t = if t1 then t2 else t3
Согласно индуктивному предположению, |Consts(t1 )| ≤ size(t1 ), |Consts(t2 )| ≤
size(t2 ), |Consts(t3 )| ≤ size(t3 ). Рассуждаем так:
Индукция по глубине:
Индукция по размеру:
Структурная индукция:
тика с малым шагом, известная также как структурная операционая семантика (Plotkin
1981). В Упражнении 3.5.17 вводится альтернативный стиль с большим шагом, называе-
мый также естественной семантикой (Kahn 1987), где единственный переход абстрактной
машины сразу вычисляет окончательное значение терма.
3.5. Вычисление 43
3.5 Вычисление
Забудем на время о натуральных числах и рассмотрим операционную семан-
тику только булевских выражений. Определение приведено на Рис. 3.1. Рас-
смотрим его в деталях.
В левом столбце Рис. 3.1 содержится грамматика, в которой определяются
два вида выражений. Во-первых, повторяется (для удобства) синтаксис тер-
мов. Во-вторых, определяется подмножество термов — множество значений,
возможных результатов вычисления. В нашем случае значениями являются
только константы true и false. На протяжении всей книги для значений ис-
пользуется метапеременная v.
В правом столбце определяется отношение вычисления 4 на термах. Оно
записывается t → t0 и читается «t переходит в t0 за один шаг вычисления».
3 Для денотационной семантики камнем преткновения оказались недетерминистские вы-
числение сохранять для варианта «с большим шагом», который описан в Упражнении 3.5.17
и напрямую сопоставляет термам их окончательные значения.
44 Глава 3. Бестиповые арифметические выражения
B (бестиповое)
Синтаксис
t ::= термы:
true константа «истина» Вычисление
false константа «ложь» if true then t2 els
if t then t else t условное if false then t2 els
выражение
t1 → t01
v ::= значения: if t1 then t2 else t3 → if
true константа «истина»
false константа «ложь»
E-IfTrue
s → false
E-If
t→u
E-If
if t then false else false → if u then false else false
46 Глава 3. Бестиповые арифметические выражения
Может показаться, что странно называть такую структуру деревом; в ней нет
никакого ветвления. Действительно, деревья вывода, обосновывающие утвер-
ждения о вычислении, всегда будут иметь такую вырожденную форму: по-
скольку в правилях вычисления всегда не более одной предпосылки, ветвле-
нию в дереве вывода взяться неоткуда. Наша терминология окажется более
осмысленной, когда рассмотрение дойдет до других индуктивно определен-
ных отношений, например, типизации — там некоторые правила могут иметь
более одной предпосылки.
То, что утверждение о вычислении t → t0 выводимо титтк существует
дерево вывода с t → t0 в корне, часто оказывается полезным при исследова-
нии свойств отношения вычисления. В частности, это немедленно подсказыва-
ет метод доказательства, называемый индукция по выводам. Доказательство
следующей теоремы может служить иллюстрацией.
структурой.
3.5. Вычисление 49
t2 → t02
(E-Funny2)
if t1 then t2 else t3 → if t1 then t02 else t3
E-PredZero
pred 0 → 0
E-Succ
succ (pred 0) → succ 0
E-Pred
pred (succ (pred 0)) → pred (succ 0)
Упражнение 3.5.14 [FF] Покажите, что Теорема 3.5.4 верна и для отноше-
ния вычисления на арифметических выражениях: если t → t0 и t → t00 , то
t0 = t00 .
Формализация операционной семантики языка заставляет нас указать по-
ведение всех термов, включая, в нашем теперешнем языке, термы вроде pred 0
и succ false. По правилам Рис. 3.2, предшествующее число от 0 определяется
как 0. С другой стороны, последователь false не вычисляется никак (другими
словами, этот терм является нормальной формой). Такие термы мы называем
тупиковыми.
Определение 3.5.15 Терм называется тупиковым, если он в нормальной фор-
ме, но не является значением.
Понятие «тупика» представляет ошибки времени выполнения в нашей про-
стой машине. С интуитивной точки зрения, оно описывает ситуации, когда
операционная семантика не знает, что делать дальше, поскольку программа
оказалась в «бессмысленном» состоянии. В более конкретной реализации язы-
ка такие состояния могут соответствовать различного рода ошибкам: обраще-
ниям по несуществующему адресу, попыткам выполнить запрещенную машин-
ную команду и т. п. Мы объединяем все эти виды неправильного поведения
под единой вывеской «тупикового состояния».
Упражнение 3.5.16 [Рекомендуется, FFF]
Другой способ формализовать бессмысленные состояния абстрактной маши-
ны состоит в том, чтобы ввести новый терм wrong («неправильное значе-
ние») и дополнить операционную семантику правилами, которые бы явным
3.5. Вычисление 51
t1 ⇓ false t3 ⇓ v3
(B-IfFalse)
if t1 then t2 else t3 ⇓ v3
t1 ⇓ nv1
(B-Succ)
succ t1 ⇓ succ nv1
t1 ⇓ 0
(B-PredZero)
pred t1 ⇓ 0
t1 ⇓ succ nv1
(B-PredSucc)
pred t1 ⇓ nv1
t1 ⇓ 0
(B-IszeroTrue)
iszero t1 ⇓ true
t1 ⇓ succ nv1
(B-IszeroFalse)
iszero t1 ⇓ false
Покажите, что семантика с малым шагом и с большим шагом для нашего
языка дают один и тот же результат, т. е., t →∗ v титтк t ⇓ v.
Упражнение 3.5.18 [FF 6→]
Предположим, что нам захотелось поменять стратегию вычисления для
нашего языка, так, чтобы ветви then и else в условном выражении вычис-
лялись (в указанном порядке) до того, как вычислится само условие. Пока-
жите, как нужно изменить правила вычисления, чтобы добиться такого
поведения.
Автор неизвестен
54 Глава 3. Бестиповые арифметические выражения
Глава 4
Реализация арифметических
выражений на языке ML
с любым инструментом; Вы свободны в выборе того языка, который Вам нравится. Однако
имейте в виду: при такой символьной обработке, какой занята программа проверки типов,
ручное управление памятью (в особенности) утомительно и чревато ошибками.
55
56 Глава 4. Реализация арифметических выражений на языке ML
4.1 Синтаксис
Сначала требуется определить OCaml-овский тип, представляющий термы.
Механизм задания типов OCaml-а делает эту задачу простой: следующее объ-
явление — прямой перевод грамматики со стр. 34.
type term =
TmTrue of i n f o
| TmFalse of i n f o
| TmIf of i n f o ∗ term ∗ term ∗ term
| TmZero of i n f o
| TmSucc of i n f o ∗ term
| TmPred of i n f o ∗ term
| TmIsZero of i n f o ∗ term
Конструкторы от TmTrue до TmIsZero называют различные виды вершин в
синтаксических деревьях типа term; тип, следующий за словом of, для каж-
дого вида указывает количество поддеревьев, которые будут подчиняться вер-
шине данного вида.
Каждая вершина абстрактного синтаксического дерева снабжена значени-
ем типа info, которое описывает место (номер символа и имя файла), откуда
происходит эта вершина. Эта информация создается процедурой синтаксиче-
ского анализа при чтении входного файла, и используется функциями рас-
печатки, чтобы сообщать пользователю местонахождение ошибки. С точки
зрения основных алгоритмов вычисления, проверки типов и т. п., эту инфор-
мацию можно было бы и вовсе не хранить; она включена в листинги, чтобы
читатели, которым захочется самим поэкспериментировать с реализациями,
видели код точно в таком виде, как он напечатан в книге.
При определении отношения вычисления нам придется проверять, является
ли терм числовым значением:
l e t rec i s n u m e r i c v a l t = match t with
TmZero (_) −> true
| TmSucc (_, t 1 ) −> i s n u m e r i c v a l t 1
| _ −> f a l s e
Это типичный пример OCaml-овского рекурсивного определения с сопо-
ставлением: isnumericval определяется как функция, которая, будучи вы-
звана с аргументом TmZero, воазвращает true; будучи применена к TmSucc
с поддеревом t1, вызывает себя рекурсивно с аргументом t1, чтобы прове-
рить, является ли он числовым значением; а будучи вызвана с любым другим
документом, возвращает false. Знаки подчеркивания в некоторых образцах
— метки «неважно», они сопоставляются с любым термом, который стоит в
указанном месте; в первых двух предложениях с их помощью игнорируются
аннотации info, а в последнем предложении с ее помощью сопоставляется лю-
бой терм. Ключевое слово rec говорит компилятору, что определение функции
рекурсивно — т. е., что идентификатор isnumericval в теле функции относит-
ся к самой функции, которую сейчас определеяют, а не к какой-либо более
ранней переменной с тем же именем.
4.2. Вычисление 57
4.2 Вычисление
Реализация отношения вычисления точно следует правилам одношагового вы-
числения из Рис. 3.1 и 3.2. Как мы видели, эти правила определяют частичную
функцию, которая, будучи применена к терму, не являющемуся значением, вы-
дает следующий шаг вычисления этого терма. Если функцию попытаться при-
менить к значению, она никакого результата не выдает. При переводе правил
вычисления на OCaml нам нужно решить, как действовать в таком случае.
Один очевидный вариант состоит в том, чтобы написать функцию одношаго-
вого вычисления eval1 так, чтобы она вызывала исключение, если ни одно из
правил невозможно применить к терму, полученному в качестве параметра.
(Другой способ действий был бы заставить вычислитель возвращать значение
типа term option, которое бы показывало, было ли вычисление успешным, и
если да, то каков результат; это тоже работало бы, но потребовало бы больше
служебного кода.) Сначала мы определяем исключение, которое можно вызы-
вать, если не применимо ни одно правило:
exception NoRuleApplies
Теперь можно написать саму функцию одношагового вычисления.
l e t rec e v a l 1 t = match t with
TmIf (_, TmTrue(_) , t2 , t 3 ) −>
t2
| TmIf (_, TmFalse (_) , t2 , t 3 ) −>
t3
| TmIf ( f i , t1 , t2 , t 3 ) −>
l e t t1 ’ = e v a l 1 t 1 in
TmIf ( f i , t1 ’ , t2 , t 3 )
| TmSucc ( f i , t 1 ) −>
l e t t1 ’ = e v a l 1 t 1 in
58 Глава 4. Реализация арифметических выражений на языке ML
TmSucc ( f i , t1 ’ )
| TmPred (_, TmZero (_) ) −>
TmZero ( dummyinfo )
| TmPred (_, TmSucc (_, nv1 ) ) when ( i s n u m e r i c v a l nv1 ) −>
nv1
| TmPred ( f i , t 1 ) −>
l e t t1 ’ = e v a l 1 t 1 in
TmPred ( f i , t1 ’ )
| TmIsZero (_, TmZero (_) ) −>
TmTrue( dummyinfo )
| TmIsZero (_, TmSucc (_, nv1 ) ) when ( i s n u m e r i c v a l nv1 ) −>
TmFalse ( dummyinfo )
| TmIsZero ( f i , t 1 ) −>
l e t t1 ’ = e v a l 1 t 1 in
TmIsZero ( f i , t1 ’ )
| _ −>
r a i s e NoRuleApplies
Заметим, что в нескольких местах мы собираем термы заново, а не пере-
делываем уже существующие. Поскольку эти новые термы не присходят из
пользовательского исходного файла, аннотации info в них не имеют смысла.
В таких термах мы используем константу dummyinfo. Для сопоставления с
аннотациями в образцах всегда используется переменная fi (file information,
«информация о файле»).
Еще одна существенная деталь в определении eval1 — использование яв-
ных выражений when в образцах, чтобы представить эффект имен метапе-
ременных вроде v и nv в правилах, определяющих отношение вычисления на
Рис. 3.1 и 3.2. Например, в выражении для вычисления TmPred(_,TmSucc(_,nv1)),
семантика OCaml-овских образцов позволяет nv1 сопоставляться с каким угод-
но термом, а это не то, чего мы хотим; добавление when (isnumericval nv1)
ограничивает правило так, чтобы оно запускалось только тогда, когда терм,
соответствующий nv1, на самом деле является числовым значением. (Мы, в
принципе, могли бы переписать исходные правила вывода в таком же стиле,
как образцы ML, переводя неявные ограничения, основанные на именах мета-
переменных, в явные условия применения правил:
t1 — числовое значение
(E-PredSucc)
pred (succ t1 ) → t1
l e t rec e v a l t =
try l e t t ’ = e v a l 1 t
in e v a l t ’
with NoRuleApplies −> t
Разумеется, наш простой вычислитель написан так, чтобы упростить срав-
нение с математическим определением вычисления, а не так, чтобы находить
нормальные формы как можно быстрее. Несколько более эффективный алго-
ритм можно получить, если взять за основу правила вычисления «с большим
шагом», как предлагается в Упражнении 4.2.2.
Бестиповое
лямбда-исчисление
61
62 Глава 5. Бестиповое лямбда-исчисление
5.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.
Лямбда-исчисление (или λ-исчисление) воплощает такой способ определе-
ния и применения функций в наиболее чистой форме. В лямбда-исчислении
все является функциями: аргументы, которые функции принимают — тоже
функции, и результат, возвращаемый функцией — опять-таки функция.
5.1. Основы 63
+ *
1 * + 3
2 3 1 2
apply
apply u
s t
λy
apply
apply x
x y
На протяжении всей книги t будет обозначать терм того исчисления, которое в данный
момент обсуждается. В начале каждой главы есть примечание, где написано, что за система
рассматривается в этой главе.
5.1. Основы 65
id = λx.x;
где запись [x 7→ t2 ]t12 означает «терм, получаемый из t12 путем замены всех
свободных вхождений x на t2 ». Например, терм (λx. x) y за шаг вычисления
переходит в y, а терм (λx. x (λx. x)) (u r) переходит в u r (λx. x). Вслед
за Чёрчем, терм вида (λx.t12 )t2 называется редексом (reducible expression, «ре-
дуцируемое выражение»), а операция переписывания редекса в соответствии
с указанным правилом называется бета-редукцией.
В течение десятков лет разработчики языков программирования и теорети-
ки изучали несколько различных стратегий вычисления в лямбда-исчислении.
66 Глава 5. Бестиповое лямбда-исчисление
Каждая стратегия определяет, какой редекс или редексы в терме может сра-
ботать на следующем шаге.4
который лучше читается, если его записать как id (id (λz. (id z))).
В этом терме три редекса:
мы. Другие называют «вычислением» только те стратегии, где какую-то роль имеет понятие
«значения», а в остальных случаях говорят о «редукции».
5.2. Программирование на языке лямбда-исчисления 67
test tru v w
= (λl. λm. λn. l m b) tru v w по определению
→ (λm. λn. tru m b) v w редукция подчеркнутого выражения
→ (λn. tru v b) w редукция подчеркнутого выражения
→ tru v w редукция подчеркнутого выражения
= (λt. λf. t) v w по определению
→ (λf. v) w редукция подчеркнутого выражения
→ v редукция подчеркнутого выражения
Несложно также определить операторы вроде логической конъюнкции:
and = λb. λc. b c fls;
То есть, and — это функция, которая, получив два логических значения b и c,
возвращает c, если b равно tru и fls, если b равно fls; таким образом, and
b c выдает tru, если и b, и c равны tru, и fls, если либо b, либо c окажутся
fls.
and tru tru;
I (λt. λf. t)
and tru fls;
I (λt. λf. f)
Упражнение 5.2.1 [F]: Оприделите логические функции or («или») и not («не»).
5.2.3 Пары
При помощи булевских констант мы можем закодировать пары значений в
виде термов:
pair = λf.λs.λb. b f s;
fst = λp. p tru;
snd = λp. p fls;
А именно, pair v w — функция, которая, будучи применена к булевскому зна-
чению b, применяет b к v и w. По определению булевских констант, при таком
вызове получится v, если b равняется tru, и w, если b равняется fls, так что
функции первой и второй проекции fst и snd можно получить, просто по-
дав в пару соответствующие булевские значения. Вот проверка утверждения
fst (pair v w) →∗ v:
fst (pair v w)
= fst ((λf.λs.λb. b f s) v w) по определению
→ fst ((λs.λb. b v s) w) редукция подчеркнутого выражения
→ fst (λb. b v w) редукция подчеркнутого выражения
= (λp. p tru) (λb. b v w) по определению
→ (λb. b v w) tru редукция подчеркнутого выражения
→ tru v w редукция подчеркнутого выражения
→∗ v как показано ранее.
co = λs. λz. z;
c1 = λs. λz. s z;
c2 = λs. λz. s (s z);
c3 = λs. λz. s (s (s z));
и т. д.
А именно, каждое число n представляется комбинатором cn , принимающим
два аргумента, s и z («последователь» и «ноль»), и применяет s к z n раз. Как
и в случае с булевскими константами и парами, такое кодирование превращает
числа в активные сущности: число n представляется функцией, которая что-то
делает n раз — своего рода активная единичная (по основанию 1) запись.
(Читатель мог уже заметить, что c0 и fls на самом деле являются одним и
тем же термом. Такие «каламбуры» часто встречаются в языках ассемблера,
где одна и та же комбинация битов может представлять множество разных
значений — целое число, число с плавающей точкой, адрес, четыре символа и
т. п., — в зависимости от того, как биты интерпретируются, а также в низко-
уровневых языках вроде C, где 0 и false тоже представляются одинаково.)
Функцию-последователь на числах Чёрча можно определить так:
scc = λn. λs. λz. s (n s z)
Терм scc — комбинатор, который принимает число Чёрча n и возвращает
другое число Чёрча, — то есть, возвращает функцию, которая принимает ар-
гументы s и z, и многократно применяет s к z. Нужное число применений s к
z мы получаем, сначала передав s и z в качестве аргументов n, а затем явным
образом применив s еще раз к результату.
pair c0 c0
ss копия +1
'
pair c0 c1
ss копия +1
'
pair c1 c2
ss копия +1
'
pair c2 c3
ss копия +1
'
pair c3 c4
..
.
рых оно ожидает, и заставляет все задержанные вычисления в его теле срабо-
тать.
5.2.6 Рекурсия
Вспомним, что терм, который не может продвинуться дальше согласно отно-
шению вычисления, называется нормальной формой. Любопытно, что у неко-
торых термов нет нормальной формы. Например, расходящийся комбинатор
содержит только один редекс, но шаг редукции этого редекса давет в резуль-
тате опять omega! Про термы, не имеющие нормальной формы, говорят, что
они расходятся.
Комбинатор omega можно обобщить до полезного терма, называемого ком-
бинатор неподвижной точки,6 и с его помощью можно определять рекурсив-
ные функции, например, factorial.7
if n=0 then 1
else n * (if n-1=0 then 1
else (n-1) * (if n-2=0 then 1
else (n-2) * ...))
if realeq n c0 then c1
else times n (if realeq (prd n) c0 then c1
else times (prd n)
(if realeq (prd (prd n)) c0 then c1
else times (prd (prd n)) ...))
6 Часто его называют Y -комбинатор с вызовом по значению. Плоткин (Plotkin 1977)
использовал обозначение Z.
7 Заметим, что более простой комбинатор неподвижной точки с вызовом по имени
Friedman and Felleisen 1996, глава 9), однако сам такой вывод достаточно хитроумен.
5.2. Программирование на языке лямбда-исчисления 75
factorial c3
= fix g c3
→ h h c3
где h = λx. g (λy. x x y)
→ g fct c3
где fct = λy. h h y
→ (λn. if realeq n c0
then c1
else times n (fct (prd n)))
c3
→ if realeq c3 c0
then c1
else times c3 (fct (prd c3 )))
→∗ times c3 (fct (prd c3 ))
→∗ times c3 (fct c02 )
где c02 поведенчески эквивалентен c2
→∗ times c3 (g fct c02 )
→∗ times c3 (times c02 (g fct c01 ))
где c01 поведенчески эквивалентен c1
(те же шаги повторяются для g fct c02 )
→∗ times c3 (times c02 (times c01 (g fct c00 )))
где c00 поведенчески эквивалентен c0
(таким же образом)
→∗ times c3 (times c02 (times c01 (if realeq c00 c0 then c1
else ...)))
→∗ times c3 (times c02 (times c01 c1 ))
→∗ c06
где c06 поведенчески эквивалентен c6
5.2.7 Представление
Прежде, чем закончить рассмотрение примеров и заняться формальным опре-
делением лямбда-исчисления, следует задаться еще одним последним вопро-
сом: что, строго говоря, означает утверждение, что числа Чёрча представля-
ют обыкновенные числа?
Чтобы ответить на этот вопрос, давайте вспомним, что такое обыкновен-
ные числа. Существует много (эквивалентных) определений; мы в этой книге
(Рис 3.2) выбрали такое:
• константа 0,
но давайте, для простоты, забудем сейчас про это различие. Можно аналогичным образом
выстроить объяснение, как именно Чёрчевы булевсие константы представляют настоящие
значения.
5.3. Формальности 77
Собирая все эти утверждения вместе, представим, что у нас есть программа,
которая проделывает некоторые сложные численные вычисления и выдает бу-
левский результат. Если мы заменим все числа и арифметические операции
лямбда-термами, которые их представляют, и запустим получившуюся про-
грамму, мы получим тот же самый результат. Таким образом, с точки зрения
воздействия на окончательные результаты программ, нет никакой разницы
между настоящими числами и их представлениями по Чёрчу.
5.3 Формальности
В оставшейся части главы мы даем точное определение синтаксиса и операци-
онной семантики лямбда-исчисления. Большая часть необходимой структуры
устроена так же, как в Главе 3 (чтобы не повторять всю эту структуру за-
ново, мы здесь определяем только чистое лямбда-исчисление, не дополненние
булевскими значениями и числами). Однако операция подстановки терма вме-
сто переменной связана с неожиданными сложностями.
5.3.1 Синтаксис
Как и в Главе 3, абстрактная грамматика, определяющая термы (на стр. 80)
должна рассматриваться как сокращенная запись индуктивно определенного
множества абстрактных синтаксических деревьев.
1. x ∈ T для всех x ∈ V;
2. Если t1 ∈ T и x ∈ V, то λx.t1 ∈ T ;
3. Если t1 ∈ T и t2 ∈ T , то t1 t2 ∈ T .
Размер терма t можно определить точно так же, как мы это сделали для
арифметических выражений в Определении 3.3.2. Кроме того, можно дать
простое индуктивное определение множества свободных переменных, встреча-
ющихся в терме.
F V (x) = {x}
F V (λx.t1 ) = F V (t1 )\{x}
F V (t1 t2 ) = F V (t1 ) ∪ F V (t2 )
5.3.2 Подстановка
Операция подстановки, когда ее требуется строго определить, оказывается до-
вольно хитроумной. В этой книге мы будем использовать два разных опреде-
ления, каждое из которых удобно для своих целей. Первое, вводимое нами в
этом разделе, краткое и интуитивно понятное, хорошо работает в примерах,
в математических определениях и доказательствах. Второе, рассматриваемое
в Главе ??, использует более сложную нотацию и зависит от альтернативного
«представления де Брюйна» для термов, где именованные переменные заменя-
ются на числовые индексы, но оно оказывается более удобным для конкретных
реализаций ML, которые обсуждаются в последующих главах.
Поучительно придти к определению подстановки, пройдя через пару неудач-
ных попыток. Попробуем сначала самое наивное рекурсивное определение. (С
формальной точки зрения, мы определяем функцию [x 7→ s] индукцией по ар-
гуенту t.):
[x 7→ s]x = s
[x 7→ s]y = y если y 6= x
[x 7→ s](λy. t1 ) = λy. [x 7→ s]t1
[x 7→ s](t1 t2 ) = ([x 7→ s]t1 ) ([x 7→ s]t2 )
Такое определение в большинстве случаев работает правильно. Например, оно
дает
[x 7→ (λz. z w)](λy. x) = λy. λz. z w
что соответствует нашей интуиции о том, как должна себя вести подстановка.
Однако при неудачном выборе имен связанных переменных это определение
ломается. Например:
[x 7→ y](λx.x) = λx.y
→ (бестиповое)
Вычисление t → t0
Синтаксис
t1 → t01
t ::= термы: (E-App1)
x переменная t1 t2 → t01 t2
λx.t абстракция
t t применение t2 → t02
(E-App2)
v1 t2 → v1 t02
v ::= значения:
λx.t значение-абстракция
(λx.t12 ) v2 → [x 7→ v2 ]t12 (E-AppAbs)
83
Приложение A
Решения избранных
упражнений
85
86 Приложение A. Решения избранных упражнений
3.5.10. Решение:
t → t0
t →∗ t0
t →∗ t
t →∗ t0 t0 →∗ t00
∗ 00
t→ t
3.5.13. Решение: (1) 3.5.4 и 3.5.11 перестают работать. 3.5.7, 3.5.8 и 3.5.12
сохраняются. (2) Теперь ломается только 3.5.4; остальные теоремы остаются
истинными. Во втором варианте интересно то, что, несмотря на потерю де-
терминированности одношагового вычисления в результате добавления нового
правила, окончательный результат многошагового вычисления по-прежнему
определяется однозначно: все дороги ведут в Рим. Доказательство этого утвер-
ждения не так уж сложно, хотя уже не настолько тривиально, как раньше.
Главное наблюдение состоит в том, что одношаговое вычисление обладает так
называемым свойством ромба:
~ @@@
r
~~~ @@
~~ @@
~~
~ @
s1 t1 A
}} AA
AA
}}} AA
} A
~}
}
s2 t2 D
zz DD
zzz DD
DD
z D"
|z
. . .z . . .C
{{ CC
{{ CC
{{ CC
}{
{ C!
s t
Тогда, используя Лемму A.0.1, мы можем «стянуть вместе» s1 и t1
rB
||| BBB
| BB
|| BB
}|
| !
s1 A t1
}} A } AAA
}} } AA
}} A } AA
}
~ } A ~} A
s2 u2 t2 DD
zz DD
zzz DD
zz DD
. . .|z ." . .C
{{ CC
{{ CC
{{ CC
}{
{ C!
s t
затем стянуть попарно s2 с u2 и u2 с t2
rB
||| BBB
| BB
|| BB
}|
| !
s1 A t1
}} A } AAA
}} } AA
}} A AA
~}} A ~} } A
s2 C u2 C t2
zz C { C { DDD
zz C { C { DD
zzz C { C { DD
D"
|
... z ! } { ! } { . . .C
{ CC
{{{ CC
{{ CC
}{{ C!
s t
и действовать так до тех пор, пока s и t не стянутся вместе, и из многих
одношаговых ромбов не получится один большой многошаговый.
88 Приложение A. Решения избранных упражнений
4.2.1. Решение: Каждый раз, когда eval вызывает себя рекурсивно, на стек
вызовов помещается обработчик try. Поскольку на каждом шаге вычисления
происходит один рекурсивный вызов, в конце концов стек переполнится. В
сущности, из-за того, что рекурсивный вызов eval обернут в try, он перестает
быть хвостовым, хотя и продолжает так выглядеть. Более правильная (хотя и
менее читабельная) версия eval такова:
l e t rec e v a l t =
l e t t ’ opt = try Some ( e v a l 1 t ) with NoRuleApplies −> None in
match t ’ opt with
Some ( t ’ ) −> e v a l t ’
| None −> t
5.2.1. Решение:
5.2.2. Решение:
5.2.3. Решение:
92 Приложение A. Решения избранных упражнений
5.2.5. Решение:
5.2.11. Решение:
ff = λf. λl.
test (isnil l)
(λx. c0 ) (λx. (plus (head l) (f (tail l)))) c0 ;
sumlist = fix ff;
t1 → t01
(E-App1)
t1 t2 → t01 t2
t2 → t02
(E-App2)
t1 t2 → t1 t02
t1 → t01
(E-Abs)
λx.t1 → λx.t01
t2 → t02
(E-App2)
nanf1 t2 → nanf1 t02
t1 → t01
(E-Abs)
λx.t1 → λx.t01
na ::= не-абстракции:
x
t1 t2
(Это определение по сравнению с остальными выглядит немного неестествен-
но. Обычно нормальный порядок вычислений определяется «как при полной
бета-редукции, только всегда выбирается самый левый, самый внешний ре-
декс».)
5.4.3. Решение:
λx.t ⇓ λx.t
t1 ⇓ λx.t12 t2 ⇓ v2 [x 7→ v2 ]t12 ⇓ v
t1 t2 ⇓ v