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

       

Prog-выражения и циклы


Противопоставление функционального и императивного стилей программирования порой напоминает войну остроконечников с тупоконечниками. Существует большое число чисто теоретических работ, исследовавших соотношения между потенциалом того и другого подхода и пришедших к заключению о формальной сводимости в обе стороны при некоторых непринципиальных ограничениях на технику программирования. Методика сведения императивных программ в функциональные заключается в определении правил разметки или переписывания схемы программы в функциональные формы. Переход от функциональных программ к императивным технически сложнее: используется интерпретация формул над некоторой специально устроенной абстрактной машиной [3,

4]. На практике переложение функциональных программ в императивные выполнить проще, чем наоборот — может не хватать близких понятий.

С практической точки зрения любые конструкции стандартных языков программирования могут быть введены как функции, дополняющие исходную систему программирования, что делает их вполне легальными средствами в рамках функционального подхода. Надо лишь четко уяснить цену такого дополнения и его преимущества, обычно связанные с наследованием решений и привлечением пользователей. В первых реализациях Лиспа были сразу предложены специальные формы и структуры данных, служащие мостом между разными стилями программирования, а заодно смягчающие недостатки исходной, слишком идеализированной, схемы интерпретации S-выражений, выстроенной для учебных и исследовательских целей. Важнейшее такого рода средство, выдержавшее испытание временем — prog-форма, списки свойств атома и деструктивные операции, расширяющие язык программирования так, что становятся возможными оптимизирующие преобразования структур данных, программ и процессов, а главное — раскрутка систем программирования [1 (не найдено)].

Применение prog-выражений позволяет писать "паскалеподобные" программы, состоящие из операторов, предназначенных для исполнения. (Точнее "алголоподобные", т.к.
появились лет за десять до паскаля. Но теперь более известен паскаль.)

Для примера prog-выражения приводится императивное определение функции (Length *), сканирующей список и вычисляющей число элементов на верхнем уровне списка. Значение функции Length — целое число. Программу можно примерно описать следующими словами (Стилизация примера от МакКарти [1 (не найдено)].):

"Это функция одного аргумента L. Она реализуется программой с двумя рабочими переменными u и v. Записать число 0 в v. Записать аргумент L в u. A: Если u содержит NIL, то программа выполнена и значением является то, что сейчас записано в v. Записать в u cdr от того, что сейчас в u. Записать в v на единицу больше того, что сейчас записано в v. Перейти к A"

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

function LENGTH (L: list) : integer; label A; var U: list; V: integer; begin V := 0; U := l; A: if null (U) then LENGTH := V; U := cdr (U); V := V+1; goto A; end;

Переписывая в виде S-выражения, получаем программу:

(defun LENGTH (L) (prog (U V) (setq V 0) (setq U L) A (cond ((null U)(return V))) (setq U (cdr U)) (setq V (+ 1 V)) (go A) ) )

(LENGTH '(A B C D)) (LENGTH ‘((X . Y) A CAR (N B) (X Y Z)))

Последние две строки содержат тесты. Их значения четыре и пять, соответственно. Prog-форма имеет структуру, подобную определениям функций и процедур в Паскале: (PROG, список рабочих переменных , последовательность операторов и атомов ...) Атом в списке является меткой, локализующей оператор, расположенный вслед за ним. В приведенном примере метка A локализует оператор, начинающийся с COND.

Первый список после символа PROG называется списком рабочими переменных. При отсутствии таковых должно быть написано NIL или (). С рабочими переменными обращаются примерно как со связанными переменными, но они не могут быть связаны ни с какими значениями через lambda.


Значение каждой рабочей переменной есть NIL, до тех пор, пока ей не будет присвоено что-нибудь другое.



Для присваивания рабочей переменной применяется форма SET. Чтобы присвоить переменной pi значение 3.14 пишется (SET (QUOTE PI)3.14). SETQ подобна SET, но она еще и блокирует вычисление первого аргумента. Поэтому (SETQ PI 3.14) — запись того же присваивания. SETQ обычно удобнее. SET и SETQ могут изменять значения любых переменных из а-списка более внешних функций. Значением SET и SETQ является значение их второго аргумента.

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

GO-форма, используемая для указания перехода (GO A) указывает, что программа продолжается оператором, помеченным атомом A, причем это A может быть и из внешнего выражения prog.

Условные выражения в качестве операторов программы обладают полезными особенностями. Если ни одно из пропозициональных выражений не истинно, то вместо указания на ошибку, происходящего во всех других случаях, программа продолжается оператором, следующим за условным выражением. Это справедливо лишь для условного выражения, находящегося на верхнем уровне Prog.

RETURN — нормальный конец программы. Аргумент return вычисляется, что и является значением программы. Никакие последующие операторы не вычисляются.

Формы Go, Set, Return могут применяться как операторы лишь на верхнем уровне PROG или внутри COND, находящегося на верхнем уровне PROG.

Если программа прошла все свои операторы, она оканчивается со значением NIL.

Prog-выражение, как и другие Лисп-функции, может быть рекурсивным.

Функция REV, обращающая список и все подсписки, столь же естественно пишется с помощью рекурсивного Prog-выражения.

function rev (x: list) :List; label A, B; var y, z: list; begin

A: if null (x) then rev := y; z := cdr (x); if atom (z) then goto B; z := rev (z);



B: y := cons (z, y); x := cdr (x); goto A; end;

Функция rev обращает все уровни списка, так что rev от (A ((B C) D)) даст ((D (C B))A).

(DEFUN rev (x) (prog (y z)

A (COND ((null x)(return y))) (setq z (CDR x)) (COND ((ATOM z)(goto B))) (setq z (rev z))

B (setq y (CONS z y)) (setq x (CDR x)) (goto A) ))

Для того чтобы форма prog была полностью законна, необходима возможность дополнять а-список рабочими переменными. Кроме того, операторы этой формы требуют специального расширения языка — в него включаются формы go, set и return, не известные вне prog. Атомы, играющие роль меток, работают как указатели помеченного блока.

Кроме того, уточнен механизм условных выражений: отсутствие истинного предиката не препятствует формированию значения cond -оператора, т.к. все операторы игнорируют выработанное значение. Это позволяет считать, что значением является Nil. Такое доопределение условного выражения приглянулось и в области обычных функций, где оно часто дает компактные формулы для рекурсии по списку.

В принципе, SET и SETQ могут быть реализованы с помощью a-списка примерно так же, как и поиск значения, только с копированием связей, расположенных ранее изменяемой переменной (см. функцию assign из лекции 3). Более эффективная реализация будет описана ниже.

(DEFUN set (x y) (assign x y Alist))

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

(setq x 'y) (set x 'NEW) (print x) (print y)

Пример 6.1.

Напечатается Y и NEW (традиционно атомы печатаются заглавными буквами).


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