Добавил:
ivanov666
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз:
Предмет:
Файл:Основы функционального программирования. Учебное пособие
.pdf
Имена, определения и контексты
Рассматриваются основные методы расширения функциональных
систем с помощью иерархии разнородных контекстов определений.
Изучаются приемы достижения удобочитаемости функциональных
программ при определении сложных функций и анализируются
особенности типовых схем связывания имен переменных с их
значениями, принятых в системах программирования. Знакомство с
методом неподвижных точек в системах рекурсивных определений
логически завершает схему выбора решений по взаимодействию имен с
определениями.
Интерпретирующая система. Реализационное
уточнение интерпретации
Эта глава предназначена для реализационного уточнения уже
известных теоретических рассуждений. Ряд уточнений показан на
примере, представляющем программу, которая определяет три функции
UNION, INTERSECTION и MEMBER, а затем применяет эти функции к
нескольким тестам [1]. На этом примере будут рассмотрены средства и
методы, обеспечивающие удобочитаемость функциональных программ
и удобство их развития при отладке. Как правило, удобство достигается
включением в систему дополнительных функций для решения проблем,
принципиальное решение которых уже обеспечено на уровне теории.
Функции UNION и INTERSECTION применяют к множествам, каждое
множество представлено в виде списка атомов. Заметим, что все
функции рекурсивны, а UNION и INTERSECTION используют
MEMBER. Схематично работу этих функций можно выразить
следующим образом:
member = lambda [a;x]
[ null[x] >> Nil ]
[ eq[a;car[x]] >> T ]
[ T >> member[a;cdr[x]] ]
union = lambda[x;y]
[ null[x] >> y ]
Основы функционального программированияЛ.В. Городняя
91

[ member[car[x];y] >> union[cdr[x];y] ]
[ T >> cons[car[x];union[cdr[x];y]] ]
intersection = lambda [x;y]
[ null[x] >> NIL ]
[ member[car[x];y] >>
cons[car[x];intersection[cdr[x];y]] ]
[ T >> intersection[cdr[x];y] ]
Определяя эти функции на Лиспе, мы используем дополнительную
псевдо-функцию DEFUN, объединяющую эффекты LAMBDA и LABEL.
Программа выглядит так:
(DEFUN MEMBER (A X)
(COND
((NULL X) Nil)
((EQ A (CAR X)) T)
(T (MEMBER A (CDR X)) )
) )
(DEFUN UNION (X Y)
(COND
((NULL X) Y)
((MEMBER (CAR X) Y) (UNION (CDR X) Y) )
(T (CONS (CAR X) (UNION (CDR X) Y))) )) )
) )
(DEFUN INTERSECTION (X Y)
(COND
((NULL X) NIL)
((MEMBER (CAR X) Y)
(CONS (CAR X) (INTERSECTION (CDR X) Y)) )
(T (INTERSECTION (CDR X) Y))
) )
(INTERSECTION '(A1 A2 A3) '(A1 A3 A5))
(UNION '(X Y Z) '(U V W X))
Эта программа предлагает вычислить пять различных форм.
Основы функционального программированияЛ.В. Городняя
92

Первые три формы сводятся к применению псевдо-функции DEFUN.
Псевдо-функция — это функция, которая выполняется ради ее
воздействия на систему, тогда как обычная функция — ради ее
значения. DEFUN заставляет функции стать определенными и
допустимыми в системе равноправно со встроенными функциями. Ее
значение — имя определяемой функции, в данном случае — MEMBER,
UNION, INTERSECTION. Более точно можно сказать, что полная
область значения псевдо-функции DEFUN включает в себя некоторые
доступные ей части системы, обеспечивающие хранение информации о
функциональных объектах, а формальное ее значение — атом,
символизирующий определение функции.
Значение четвертой формы — (A1 A3). Значение пятой формы — (Y Z C
B D X). Анализ пути, по которому выполняется рекурсия, показывает,
почему элементы множества появляются именно в таком порядке.
В этом примере продемонстрировано несколько элементарных правил
написания функциональных программ, сложившихся при реализации
интерпретатора Лисп 1.5 в дополнение к идеализированным правилам,
сформулированным в строгой теории Лиспа. (С точностью до разницы
между EVALQUOTE — EVAL.)
1. Программа состоит из последовательности вычисляемых форм.
Если форма список, то ее первый элемент интерпретируется как
функция. Остальные элементы списка — аргументы для этой
функции. Они вычисляются с помощью EVAL, а функция
применяется к ним с помощью APPLY, и полученное значение
выводится как результат программы.
2. Особого формата для записи программ не существует. Границы
строк игнорируются. Формат программы, включая
идентификацию, выбран просто для удобства чтения.
3. Любое число пробелов и концов строк можно разместить в любой
точке программы, но не внутри атома.
4. Не используются (QUOTE T), (QUOTE NIL). Вместо них
применяется T, NIL, что влечет за собой соответствующее
изменение определения EVAL.
5. Атомы должны начинаться с букв, чтобы их можно было легко
отличать от чисел.
Основы функционального программированияЛ.В. Городняя
93

6. Может использоваться точечная нотация. Любое число пробелов
перед или после точки, свыше одного, будет игнорироваться (один
пробел нужен обязательно при соседстве с атомом).
7. Точечные пары могут появляться как элементы списка, и списки
могут быть элементами точечных пар.
((A . B) X (C . (E F D)))
— допустимое S-выражение. Оно может быть записано как
((A . B) . ( X . ((C . (E . ( F . (D . Nil)))) . Nil)))
или
((A . B) X (C E F D))
8. Форма типа (A B C . D) есть сокращение для (A . ( B .
( C . D) )). Любая другая расстановка точек на одном уровне
есть ошибка, например (A. B C).
9. Набор основных функций предоставляется системой. Другие
функции могут быть введены программистом. Порядок введения
функций не имеет значения. Любая функция может
использоваться в определении другой функции.
Вывод S-выражений на печать и в файлы выполняет псевдофункция PRINT, чтение данных обеспечивает псевдо-функция
READ. Программа из файла может быть загружена псевдофункцией LOAD.
(LOAD 'TEST.LST)
(PRINT (INTERSECTION '(A1 A2 A3) '(Al A3 A5)) )
(PRINT (UNION '(X Y Z) '(U V W X)) )
(PRINT (UNION (READ) '(1 2 3 4)) )
; объединение вводимого списка со списком
; '(1 2 3 4)
Именование значений и подвыражений
Основы функционального программированияЛ.В. Городняя
94

Переменные
Переменная — это символ, который используется для представления
аргумента функции.
Таким образом, возвращаясь к Дж.Маккарти [1]:
"Можно написать "a + b, где a = 341 и b = 216". В такой ситуации не
может быть недоразумений, и все согласятся, что ответ есть 557. Чтобы
получить этот результат, необходимо заменить переменные
фактическими значениями, и затем сложить два числа (на арифмометре,
например).
Одна из причин, по которой в этом случае не возникает недоразумений,
состоит в том, что "a" и "b" не есть приемлемые входы для арифмометра,
и следовательно, очевидно, что они только представляют фактические
аргументы. При символьных вычислениях ситуация может быть более
сложной. Атом может быть как переменной, так и фактическим
аргументом. К дальнейшему усложнению приводит то обстоятельство,
что некоторые из аргументов могут быть переменными, вычисляемыми
внутри вызова другой функции. В таких случаях интуитивный подход
может подвести. Для эффективного программирования в
функциональном стиле необходимо более точное понимание
формализмов.
Чтобы не обескураживать читателей, следует заметить, что здесь ничего
принципиально нового нет. Все, что сейчас рассматривается, может
быть логически выведено из правил представления программ в виде Sвыражений или из определения универсальных функций eval/apply, и
является их непосредственным следствием, возможно, не вполне
очевидным.
Любой формализм для переменных сводится к лямбда-обозначению.
Часть интерпретатора, которая при вычислении функций связывает
переменные, называется APPLY. Когда APPLY встречает функцию,
начинающуюся с LAMBDA, список переменных попарно связывается со
списком аргументов и добавляется к началу а-списка. При вычислении
функции могут быть обнаружены переменные. Они вычисляются
Основы функционального программированияЛ.В. Городняя
95

поиском в а-списке. Если переменная встречается несколько раз, то
используется последнее или самое новое значение. Часть
интерпретатора, которая делает это, называется EVAL.
Проиллюстрируем данное рассуждение на примере. Предположим, что
интерпретатор получает следующее S-выражение:
((LAMBDA (X Y) (CONS X Y)) 'A 'B)
Функция: ((LAMBDA (X Y) (CONS X Y)) 'A 'B)
Аргументы: (A B)
EVAL через EVAL-A передает эти аргументы функции APPLY. (См. лек.
3).
(APPLY #'(LAMBDA (X Y) (CONS X Y)) '(A B) Nil )
APPLY свяжет переменные и передаст функцию и удлинившийся асписок EVAL для вычисления.
(EVAL '(CONS X Y) ' ((X . A) (Y . B) . Nil))
EVAL вычисляет переменные и сразу передает их консолидации функции CONS, строящей из них бинарный узел.
(Cons 'A 'B) = (A . B)
EVAL вычисляет переменные и сразу передает их консолидации,
строящей из них бинарный узел.
Реальный интерпретатор пропускает один шаг, требуемый формальным
определением универсальных функций".
На практике сложилась традиция включать в систему функционального
программирования специальные функции, обеспечивающие иерархию
контекстов, в которых переменные обладают определенным значением.
Для Clisp это LET и LET *.
Основы функционального программированияЛ.В. Городняя
96

LET сопоставляет локальным переменным независимые выражения. С
ее помощью можно вынести из сложного определения любые
совпадающие подвыражения.
(DEFUN UNION (x y)
(LET ( (a-x (CAR x))
(d-x (CDR x))
) ; конец списка локальных именованных значений
(COND ((NULL x) y)
((MEMBER a-x y) (UNION d-x y) ) ; использование локальных
(T (CONS a-x (UNION d-x y)) ) ; значений из контекста
) ) ; завершение контекста LET
)
LET * — сопоставляет локальным переменным взаимосвязанные
выражения. Она позволяет дозировать сложность именуемых
подвыражений.
(DEFUN MEMBER (a x)
(LET* ( (N-X (NULL x))
(a-x (CAR x))
(d-x (CDR x))
(e-car (EQ a a-x))
) ; список локально именованных выражений
(COND (N-X Nil) ; использование
(E-CAR T) ; именованных
(T (MEMBER A D-X)) ; выражений
) ) ; выход из контекста именованных выражений
)
(Эквивалентность с точностью до побочного эффекта.)
Глобальные переменные можно объявить с помощью специальной
функции DEFPARAMETER.
(DEFPARAMETER GLOB '(a b c))
Основы функционального программированияЛ.В. Городняя
97

Значение такой переменной доступно в любом контексте, оно может
быть переопределено. Возможно оттеснение одноименными
локальными переменными с восстановлением при выходе из
соответствующих контекстов. При выполнении кода программ:
(LET ((GLOB 12))(PRINT GLOB))
(PRINT GLOB)
напечатано будет:
12
(A B C)
Константы
Дж.Маккарти обращает внимание [1]:"Иногда говорят, что константы
представляют сами себя, в противоположность переменным,
представляющим что-то другое. Это не вполне точно, поскольку
обычно константы представляют как a, b, c, ..., а переменные как x, y, z,
... — но и то, и другое выглядит как атом. Удобнее говорить, что одна
переменная ближе к константам, чем другая, если она связана на более
высоком уровне и ее значение изменяется не столь часто.
Обычно переменная считается связанной в области действия лямбдаконструктора функции, который связывает переменную внутри тела
определения функции путем размещения пары из имени и значения в
начале а-списка. В том случае, когда переменная всегда имеет
определенное значение независимо от текущего состояния а-списка, она
будет называться константой. Такую неизменяемую связь можно
установить, помещая пару (a . v) в конец a-списка. Но в реальных
системах это организовано с помощью так называемого списка свойств
атома, являющегося представлением переменной. Каждый атом имеет
свой p-список (property list), доступный через хэш-таблицу
идентификаторов, что действует эффективнее, чем a-список. С каждым
атомом связана специальная структура данных, в которой размещается
имя атома, его значение, определение функции, представляемой этим
же атомом, и список произвольных свойств, помеченных
индикаторами. При вычислении переменных EVAL исследует эту
Основы функционального программированияЛ.В. Городняя
98

структуру до выполнения поиска в а-списке. Такое устройство
переменных не позволяет им служить в а-списке".
Константы могут быть заданы программистом. В системе Clisp чтобы
переменная X стала обозначением для (A B C D), надо воспользоваться
псевдо-функцией DEFCONSTANT.
(DefConstant X '(A B C D))
Особый интерес представляет тип констант, которые всегда обозначают
себя, Nil — пример такой константы. Такие константы как T, Nil и другие
самоопределимые константы (числа, строки) не могут использоваться в
качестве переменных. Cмысл чисел и строк не может быть изменен с
помощью DEFCONSTANT.
Функции
Ситуация, когда атом обозначает функцию, реализационно подобна той,
в которой атом обозначает аргумент. Если функция рекурсивна, то ей
надо дать имя. Теоретически это делается с помощью формы LABEL,
которая связывает название с определением функции в ассоциативном
списке (а-списке). Название связано с определением функции точно так
же, как переменная связана со своим значением. На практике LABEL
используется редко. Удобнее связывать название с определением другим
способом. Это делается путем размещения определения функции в
списке свойств атома ( р-список ), символизирующего ее название.
Выполняет данную операцию псевдо-функция DEFUN, описанная в
начале этой лекции. Когда APPLY интерпретирует функцию,
представленную атомом, она исследует р-список до поиска в а-списке.
Таким образом, DEFUN будет опережать LABEL.
Тот факт, что большинство функций — константы, определенные
программистом, а не переменные, изменяемые программой, вызван
отнюдь не каким-либо недостатком функционального
программирования. Напротив, он указывает на потенциал подхода,
который мы не научились использовать лучшим образом. (Работы по
теории компиляции, оптимизации и верификации программ,
смешанным вычислениям, суперпрограммированию и т.п. активно
используют средства функционального программирования.)
Основы функционального программированияЛ.В. Городняя
99

Системы функционального программирования обеспечивают
Основы функционального программированияЛ.В. Городняя
100
возможность манипулирования функциональными переменными так
же, как и обычными. Но организуется это с помощью ряда специальных
функций, осуществляющих переход от символов, изображающих
функции, к функциональным объектам, представленным этими
символами. Такой переход может быть реализован в рамках любого
языка программирования, но на Лиспе он выглядит естественно и
поддерживается в любой Лисп-системе. Рассмотрим средства Clisp,
обеспечивающие структурирование функциональных объектов.
LABELS — позволяет из списка определений функций формировать
контекст, в котором вычисляются выражения.
FLET — специальная функция, позволяющая вводить локальные
нерекурсивные функции.
DEFUN — позволяет вводить новые определения на текущем уровне.
(LABELS ( (INTERSECTION (x y)
(LET* ( (N-X (NULL x))
(MEM-CAR (MEMBER (CAR x) y))
(INT #'INTERSECTION)
) ; конец списка локальных выражений let*
(FLET ((F-TAIL (FN sx sy)
(APPLY FN (LIST (CDR sx) sy)) )
(CONS-F-TAIL (FN sx sy)
(CONS (CAR sx)
(APPLY FN (LIST (CDR sx) sy))
)) ) ; конец списка нерекурсивных функций FLET
(COND (N-X NIL) ; выражение, использующее
(MEM-CAR (cons-f-tail INT x y) ) ; локальные определения функций
(T (f-tail INT x y)) ) ; из контекстов FLET и
; LABELS
) ; выход из контекста FLET
) ; выход из контекста LET*
) ; завершено определение INTERSECTION
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]
