Интерпретация и компиляция функциональных программ
Рассмотрим реализацию энергичного интерпретатора функциональных программ, представленных в виде S-выражений. Ограничим эту реализацию базовыми функциями для работы со списками и управляющими конструкциями. Интерпретатор можно легко перевести на любой язык программирования и расширить, добавляя новые функции по аналогии.
Контекст проще всего представить в виде двух списков: n – список имен и v – список значений. Например, пусть n=((X Y Z) (A X F)) и v=((1 (A B) –127) (C 3 ((lambda (X) (+ X 1)) . …))), тогда контекст имеет вид {X → 1, Y → (A B), Z → –127, A → C, F → [(λ (X) (+ X 1)), …]}. Список n состоит из нескольких подсписков, которые являются наборами связей, добавляемых на разных этапах вычислений. В этом примере связь X → 3 временно вытеснена связью X → 1. Функциональное замыкание будем представлять в виде S-выражения (λ . (n . v)), где λ – λ-выражение, а n и v – состояние контекста в момент определения функции (значения свободных переменных также содержатся в этом контексте).
Для доступа к контексту используется функция ассоц(x,n,v), которая по имени x из списка имен n выдает соответствующее значение из списка значений v. Если имени в списке n нет, то результат не определен.
ассоц(x,n,v)=если содержит(x,car(n)) то найти(x,car(n),car(v))
иначе ассоц(x,cdr(n),cdr(v));
найти(x,s,t)=если eq(x,car(s)) то car(t) иначе найти(x,cdr(s),cdr(t))
Кроме того, потребуются дополнительные функции для получения списка переменных, определяемых в блоке локальных определений, для получения списка выражений этого блока и для вычисления списка выражений.
перем(s)=если null(s) то ′() иначе cons(caar(s),перем(cdr(s)));
выр(s)=если null(s) то ‘() иначе cons(cadar(s),выр(cdr(s)));
вычспис(e,n,v)=если null(e) то ′()
иначе cons(вычислить(car(e),n,v), вычспис(cdr(e),n,v))
Основной функцией программы является функция вычислить(e,n,v), которая вычисляет S-выражение e в контексте n и v.
вычислить(e,n,v)=
если atom(e) то ассоц(е,n,v)
иначе если eq(car(e),′quote) то cadr(e)
/* обработка базовых функций работы со списками */
иначе если eq(car(e),′car) то car(вычислить(cadr(e),n,v))
иначе если eq(car(e),′cdr) то cdr(вычислить(cadr(e),n,v))
иначе если eq(car(e),′atom) то atom(вычислить(cadr(e),n,v))
иначе если eq(car(e),′cons) то
cons(вычислить(cadr(e),n,v), вычислить(caddr(e),n,v))
иначе если eq(car(e),′eq) то
eq(вычислить(cadr(e),n,v), вычислить(caddr(e),n,v))
/* обработка управляющих предложений */
иначе если eq(car(e),′if) то
вычислить(если вычислить(cadr(e),n,v) то
caddr(e) иначе cadddr(e) ,n,v)
иначе если eq(car(e),′λ) то
cons(e,cons(n,v))
иначе если eq(car(e),′let) то
{вычислить(caddr(e),cons(y,n),cons(z,v))
где y=перем(cadr(e));
z=вычспис(выр(cadr(e)),n,v)}
иначе если eq(car(e),′letrec) то
{{вычислить(caddr(e),cons(y,n),setcar(w,z))
где z=вычспис(выр(cadr(e)),cons(y,n),w)}
где y=перем(cadr(e));
w=cons(‘(),v)}}
иначе /* вызов функции */
{вычислить(caddar(с),cons(cadar(c),cadr(c)),cons(z,cddr(c)))
где с=вычислить(car(e),n,v);
z= вычспис(cdr(e),n,v)}
При выполнении рекурсивного блока (letrec ((x1 e1) … (xk ek)) e) для создания контекста используется псевдофункция setcar(x,y), которая разрушает уже существующую структуру данных, что запрещено в функциональном программировании. Действие этой функции заключается в замене первого элемента списка x на y. В языке Scheme эта функция имеет имя set–car!. С ее помощью можно построить циклические структуры, что и требуется для интерпретации рекурсивного блока. В принципе можно определить вычисление блока letrec без использования разрушающей псевдофункции, но для этого потребуется ленивая семантика языка. При вычислении значений выражений e1, …, ek значения для x1, …, xk еще недоступны, поэтому выражения e1, …, ek не должны требовать немедленного доступа к x1, …, xk, то есть должны быть функциями.
Чем больше возможностей (базовых функций, управляющих конструкций, способов вычисления аргументов функции) мы будем включать в язык, тем более сложным будет интерпретатор и сложнее его переделка. Обычно программа на языке LISP или Scheme преобразуется в программу на языке гораздо более низкого уровня, похожем на машинный язык, который может выполняться с гораздо большей скоростью. Интерпретатор этого простого языка написать гораздо легче и его легче изменять. Компилятор может быть написан на базовом языке и преобразован вручную. Все последующие версии компилятора транслируются с использованием предыдущей версии.
SECD-машина для интерпретации LISP-программ получила свое название по начальным буквам ее регистров. Каждый из регистров содержит указатель на S-выражение.
Таблица 2
Назначение регистров SECD-машины
Регистр | Назначение |
Stack | Стек для размещения промежуточных результатов во время вычисления выражения |
Environment | Служит для хранения значений, связываемых с переменными во время вычисления, соответствует списку значений v в интерпретаторе. |
Control list | Указатель на выполняемую команду программы |
Dump | Стек для хранения других регистров при вызове новой функции |
Для ускорения доступа к значениям переменных вместо последовательного поиска в контексте используются индексные пары (i . j), где i – номер списка, а j – номер элемента в списке. Определение индекса значения происходит во время компиляции.
Программа (cons (+ x 1) (cdr y)) может быть преобразована компилятором в следующий код: ((LOAD 0 . 0) (QUOTE . 1) + (LOAD 0 . 1) CDR CONS). Выполнение команд SECD-машины легко описать с помощью таблицы 3.
Таблица 3
Начальное состояние регистров | Конечное состояние регистров |
S| | E| | C| | D| | S| | E| | C| | D |
s | e | ((LOAD i . j) . c) | d | (v . s) | e | c | d |
s | e | ((QUOTE . v) . c) | d | (v . s) | e | c | d |
(a b . s) | e | (+ . c) | d | (a+b . s) | e | c | d |
((a . b) . s) | e | (CDR . c) | d | (b . s) | e | c | d |
(a b . s) | e | (CONS . c) | d | ((a . b) . s) | e | c | d |
Кроме S-выражений, функциональную программу можно представить также в виде графа (рис. 4), для обработки которого используется G-машина, и потокового графа (рис. 5), для обработки которого используется потоковая машина.
 |  |
Рис. 4 | Рис. 5 |
В функциональном языке программист не заботится явно о размещении новых объектов в памяти и удалении их после того, как объект перестанет быть необходимым. Поэтому очень важной является сборка мусора, которая запускается каждый раз при нехватке памяти для дальнейшей работы функциональной программы. Для SECD-машины сборка мусора состоит в том, что все ячейки памяти, к которым нет прямых или косвенных ссылок из регистров, переводятся в список свободных ячеек памяти. Основные классы сборщиков мусора описаны в главе 16 книги Филд А., Харрисон П. "Функциональное программирование".