Основы функционального программирования


Сборка системы и ее рабочий цикл


Моделирование Лиспа или другого языка программирования на идеальном базовом Лиспе вполне может послужить иллюстрацией определения операционной семантики языков программирования [8].

Традиционно система программирования для языка Лисп содержит пару интерпретатор-компилятор. Между этими понятиями трудно установить формальную границу. Любой интерпретатор содержит элементы, реализация которых описывается в машинных терминах: структура памяти, реализация двоичных деревьев и т. п. Любая компилированная программа содержит интерпретируемые элементы: например, обращения к файловой системе и другим элементам ОС. На практике достоинства интерпретации проявляются при отладке программ, а преимущества компиляции — при эксплуатации готового программного продукта. Более подробное обсуждение этой темы заслуживает отдельного разговора.

Теория программирования утверждает, что определение компилятора может быть выведено из определения интерпретатора методами смешанных вычислений [9]. Это методы, допускающие частичную обработку программ при неполных или избыточных данных с построением остаточной программы, которую можно применять по мере уточнения данных. Компилятор по такой методике получается как остаточная программа при смешанном вычислении интерпретатора. Теоретически для определения языка программирования достаточно построить определение интерпретатора, хотя практичность реальной системы программирования обычно обеспечивается оптимизирующей компиляцией и кодогенерацией программ [9]. Но здесь речь идет не об эффективном компиляторе, а лишь о понятном описании семантики.

Ядро интерпретатора языка Лисп может быть реализовано следующим образом:

  • выбирается реализация списков в виде бинарных деревьев, листья которого рассматриваются как атомы, а узлы используются для выстраивания списков (левые ветви — элементы списка, правые — продолжение списка или конец списка, т.е. пустой список);
  • выбирается реализация атомов как объектов, внутренняя структура которых при определении и исполнении функций не всегда существенна, но при необходимости доступна специальным операциям;
  • встраивается специальный атом NIL, являющийся реализацией пустого списка ();
  • встраивается операция, связывающая различные данные с атомами, и ассоциируется с атомом LABEL;
  • определяются правила доступа к параметрам встроенных операций с размещением их результата и встраивается специальная операция (монитор), выполняющая применение операций к правильно размещенным аргументам (SUBR);
  • встраивается операция, строящая из атомов и списков более сложные структуры (списки и узлы из любых элементов), и ассоциируется с атомом CONS;
  • встраиваются операции, выполняющие разбор и анализ структур, и ассоциируются с атомами CAR, CDR, ATOM, EQ, представляющими эти операции;
  • встраиваются специальные операции (псевдо-функции), выполняющие блокировку вычислений, выбор ветви и конструирование определений функций, и ассоциируются с атомами QUOTE, COND и LAMBDA, соответственно;
  • универсальная функция ассоциируется с EVAL;
  • определяется рабочий цикл передачи данных интерпретатору и вывода результата интерпретации.


Такое ядро представляет собой машинно-зависимую часть Лисп-интерпретатора. Встраивание операции в ядро системы — это включение в его реализацию исполняемого кода, который является реализацией этой операции. Адрес такого кода ассоциируется с именем атома, с помощью которого будет организовано выполнение операции при интерпретации программ.

При ассоциировании атомов с произвольной информацией можно использовать специально организованный ассоциативный список, построенный из пар, содержащих атомы и их определения. Например, ассоциативный список

((T . T )(NIL . NIL))

обеспечивает значение T и NIL в соответствии с семантикой базового Лиспа, список

((ОДИН . 1)(ДВА . 2))

дает символьные имена числовым значениям, а список

((ГОЛОВА . CAR)(ХВОСТ . CDR)(УЗЕЛ . CONS))

— синонимы для обозначения базовых операций Лиспа.

Ассоциативный список работает как стек: при многократных определениях работает самое новое, т.е. расположенное ближе к началу списка. Если мы знаем адреса кода операций, встроенных в ядро системы, то можем соответствие между именами операций и адресами их кода хранить в ассоциативном списке. Можно считать, что начальное состояние ассоциативного списка содержит таблицу соответствия между именами и адресами операций.

Основой определения интерпретатора является функция EVAL (evaluation), вычисляющая произвольные выражения языка с учетом состояния ассоциативного списка AL. Спецификация такой функции для базового Лиспа может быть проиллюстрирована следующими примерами:

(EVAL NIL AL ) => NIL (EVAL T AL ) => T (EVAL 'X AL ) => (CADR (ASSOC X AL)) (EVAL '(QOUTE EXPR ) AL ) => EXPR (EVAL '(COND ((T YES) ... )) AL ) => YES (EVAL '(CSETQ X Y EXPR ) AL ) => (EVAL EXPR (CONS '(X Y ) AL)) (EVAL '(CAR A) '((A (1 2 3))(NIL NIL)) ) => 1

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



При написании на базовом Лиспе определения функции EVAL согласно приведенной выше спецификации, способной от данного списочного представления выражения E перейти к его значению с учетом заданного ассоциативного списка AL, хранящего значения атомов, мы несколько отступаем от ранее данных определений, с тем чтобы более явно выделить линии сборки системы.

(DEFUN EVAL (e al ) (COND ((MEMBER e '(NIL T )) e ) ((ATOM e ) ((LAMBDA (v ) (COND (v (CADR v ) ) (T (ERROR 'undefdvalue )) )) (ASSOC e al ) ) ) ((EQ (CAR e) 'QUOTE ) (CAR (CDR e )) ) ((EQ (CAR e) 'COND ) (EVCON (CDR e ) al ) ) ((EQ (CAR e) 'LET ) (EVAL (CADDDR e ) (CONS (CONS (CADR e ) (CONS (EVAL (CADDR e ) al ) NIL) ) al ) )) (T (APPLY (CAR e) (EVLIS (CDR e) al ) al ) ) ))

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

Определение функции APPLY работает при условии, что функция SUBR осуществляет применение встроенных функций к их аргументам, заданным в виде списка значений.

(DEFUN APPLY (fn args al ) (COND ((MEMBER fn '(CAR CDR CONS ATOM EQ )) (SUBR (CADR (ASSOC fn al)) args )) ((EQ fn NIL) NIL) ((ATOM fn ) (APPLY (EVAL fn al ) args al )) ((EQ (CAR fn ) 'LAMBDA ) (EVAL (CADDR fn ) (APPEND (PAIR (CADR fn ) args ) al ) ) ) ((EQ (CAR fn) 'LABEL ) (APPLY (CADDR fn) args (CONS (CDR fn ) al ) ) ) (T (ERROR- 'undefined_function)) ) )

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

Можно еще поработать с таким определением интерпретатора и более четко локализовать его зависимость от четырех различных категорий объектов: самоопределяемые атомы — (NIL T 1 2 3 ... ), базовые операции над данными языка, обрабатывающие предварительно вычисленные аргументы, — (CAR CDR CONS ATOM EQ ... ), специальные функции, управляющие обработкой аргументов без их предварительного вычисления, — (QUOTE COND LET ...) и конструкторы функций, строящие функциональные значения из обычных выражений, — (LAMBDA LABEL... ).



Желающие могут поэкспериментировать с самодельным интерпретатором, превращая его в модель ядра любого языка программирования, используя какую-нибудь Лисп-систему, например GNU Clisp (с точностью до имен отдельных функций).

Упражнение 9.1. Пусть READ и PRINT — встроенные функции, обеспечивающие прием с клавиатуры и вывод на экран произвольных данных языка Лисп. Напишите определение рабочего цикла интерпретации последовательности выражений.



Ответ.

(DEFINE CIRCLE (al ) (PRINT (EVAL (PRINT (READ )) al )) (CIRCLE al ) )

«Но оно же зациклится!» — скажете вы и будете правы.

Но это не помешает эксперименту, ведь в нашем распоряжении имеется конец файла Ctrl-Z или встроенная операция завершения процесса типа BYE, EXEC, SYSTEM либо что-то подобное.

Упражнение 9.2. Полученные 50 строк определения Лисп-интерпретатора и его вспомогательных функций содержит 1263 символа, если не считать пробелы в начале строк. Попробуйте написать сравнимое определение на каком-нибудь знакомом вам языке программирования.


Содержание раздела