Динамическое управление вычислением

THROW, CATCH, а также BLOCK.

Эти управляющие формы (кроме QUOTE и лямбда-вызова, а также вызовов функций), в основном, используются в теле лямбда-выражений, определяющих функции.

Предложение LET используется для создания связи переменных внутри формы:

 

(LET ((пep1 знач1) (пер2 знач2)...) форма1 форма2 ...).

 

При вычислении этого выражения статические переменные пep1, пер2, ... связываются (одновременно) с соответствующими значениями знач1, знач2, ..., а затем вычисляются значения форм форма1, форма2, ... Значение последней формы возвращается как общий результат. Форма LET* отличается от LET лишь тем, что связывание переменных и вычисление форм происходит не одновременно, а последовательно, вначале 1-е, потом 2-е и т.д.

Например:

(let*((x2)(y(*3x)))

(list x у)

Результат: (2 6).

Предложения PROG1, PROG2 и PROGN позволяют организовывать последовательные вычисления из нескольких вычисляемых форм:

 

(PROG1 форма1 форма2 ... формаn)

(PROG2 форма1 форма2 .. формаn)

(PROGN форма1 форма2 . формаn).

 

Различие этих форм лишь в возвращаемых ими в качестве общего значения результатах. Форма PROG1 возвращает значение формы1, PROG2-формы2, PROGN -последней формы n.

Например:

(progn (setq x 2) (setq у (* 3 х)))

Результат: 6.

Предложение COND является основным средством разветвления обработки. Структура условного предложения такова:

 

(COND (р1 а1) (р2 а2)... (pn an)).

 

pi - это предикаты (выражения-условия, которые могут быть либо истинными (Т), либо ложными (NIL)). Их значения вычисляются слева направо, пока не будет получено значение «истина» (Т), затем вычисляется и возвращается в качестве результата результирующее выражение ai. соответствующее 1-му истинному предикату pi. Если истинного предиката нет. то значение COND - NIL. Форма ai для соответствующего предиката может отсутствовать (тогда возвращается значение этого предиката в случае его истинности), или, наоборот, может быть задана последовательность форм для предиката pi - тогда эти формы вычисляются последовательно и возвращается значение последней.

В следующем примере с помощью предложения COND определена функция, устанавливающая тип выражения:

(defun тип (1)

(cond ((null 1) 'пусто)

((atom 1) 'атом)

(t 'список)))

Результат: ТИП.

Примеры применения этой функции:

(тип ' (a b с))

Результат: СПИСОК.

(тип (atom ' (а т ом)))

Результат: ПУСТО.

Для организации ветвления можно использовать и формулы IF, WHEN, UNLESS:

 


(IF условие то-форма иначе-форма),

 

что эквивалентно

 

(COND (условие то-форма) (Т иначе форма));

(WHEN условие форма1 форма2 ...),

 

что эквивалентно

 

(UNLESS (NOT условие) форма! форма2 ...)

или

(COND (условие форма1 форма2 ...)).

 

Можно применять и выбирающее предложение CASE:

 

(CASE ключ (список ключей1 форма11 форма12 ...)

(список ключей2 форма21 форма22 . . .)

 

В этой форме сначала вычисляется значение ключевой формы «ключ», затем происходит сравнение с элементами списков ключей и, если найдено значение ключевой формы, вычисляется последовательность соответствующих форм, значение последней из которых возвращается как значение всего выражения CASE.

Предложения PROG, GO и RETURN аналогичны конструкциям неструктурных языков программирования (типа FORTRAN, Бейсик); пользоваться ими не рекомендуется.

РЕКУРСИЯ И ЦИКЛ В ПРОГРАММАХ НА ЛИСПЕ

В «чистом» функциональном программировании организация повторяющихся вычислений должна происходить лишь с помощью условных предложений и определения рекурсивных, вызывающих самих себя, функций. Рассмотрим в качестве примера функцию, просто определяемую через рекурсию, - факториал n!=1*2 * 3 *...* (n-1) * n = (n-1)! т n (0! = 1 по определению):

 

(defun ! (n) (if(= п 0) 1 (* п (! (. п 1))))) .

 

Имя функции - "!",ее аргументом является переменная n. Лямбда-выражение, определяющее функцию, представляет собой условную if-форму, которая в случае n=0 возвращает 1, а в противном случае вычисляет произведение n и результата вызова этой же функции ! для аргумента n-1.

Пример вызова этой функции:

(!5)

Результат: 120.

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

 

(DO ((nepi знач! шаг1) (пер2 знач2 шаг 2) ...)

(условие-окончания форма11 форма12 ...)

форма21 форма22 ...)

 

Вычисление предложения DO начинается с присваивания переменным пep1, пер2, ... начальных значений знач1, знач2, . . . соответственно; потом вычисляется условие окончания и, если оно истинно, последовательно вычисляются формы форма1i, и значение последней возвращается как результат DO-предложения. В противном случае вычисляются формы форма2i из тела предложения DO, затем значения переменных пep1, пер2, . . . изменяются на величину шага шаг1, шаг2, ... и все повторяется.

Для примера с помощью предложения DO определим функцию expt, вычисляющую n-ю степень числа х (n - целое положительное):

 

(defun expt (х n)

(do ((результат 1)) ; начальное значение

((= n 0 ) результат ) ; условие окончания

(setq результат (* результат х))

(setqn(^nl))))

 

Результат задания функции: EXPT.

Пример вызова:

(expt 2 3)

Результат: 8.

Итеративные (циклические) и рекурсивные программы теоретически одинаковы по своим вычислительным возможностям, однако свойства итеративных и рекурсивных вариантов программ могут существенно различаться. Рекурсивные программы более короткие и содержательные. Особенно полезно использовать рекурсию в тех случаях, когда решаемая задача и обрабатываемые данные по своей сути рекурсивны, например, при обработке списков, так как списки могут рекурсивно содержать подсписки, при работе с другими динамическими структурами, которые заранее не полностью известны. Рекурсивные процедуры играют важнейшую роль почти во всех программах, связанных с искусственным интеллектом.

ВВОД-ВЫВОД ДАННЫХ

До сих пор рассматривался ввод и вывод данных в лисповских программах через параметры функций и свободные переменные. Для организации диалога человека с программой в Лиспе существуют специальные функции READ и PRINT.

Для вывода результатов можно использовать функцию PRINT. Это функция с одним аргументом, которая сначала вычисляет значение аргумента, а затем выводит это значение.

Например:

(PRINT (* 2 2))

Результат: 4.

Перед выводом происходит переход на новую строку.

Функция READ читает и возвращает выражение: (READ). Как только интерпретатор встречает такое предложение, вычисления приостанавливаются до тех пор, пока не будет введен какой-либо символ или целиком выражение. Аргументов у функции READ нет, ее использование построено на побочном эффекте, состоящем именно во вводе выражения. Прочитанное выражение можно сохранить для следующего использования и обработки, например, так:

 

(setq input (read));

 

прочитанное READ выражение присваивается переменной input.

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

Для форматного вывода (в соответствии с некоторым образом) существует функция FORMAT, обладающая гибкими возможностями, описанными в руководствах по языку Лисп.

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

ПРИМЕР ПРОГРАММИРОВАНИЯ НА ЛИСПЕ

Рассмотрим в качестве примера программирования на Лиспе менее элементарную классическую задачу, носящую название игры в «ханойские башни».

Игра состоит в следующем. Используются три вертикальных стержня А, В, С и набор N дисков разного диаметра с отверстием посередине (так что их можно надевать на стержни). В начальном положении все диски надеты на стержень А по порядку убывания диаметров: внизу самый большой, над ним - поменьше и т.д., а наверху - самый маленький. Целью является перенос всех дисков со стержня А на стержень В по следующим правилам:

1) за один раз можно перенести только один диск;

2) больший по размеру диск нельзя положить на меньший;

3) третий стержень С можно использовать как вспомогательный. Алгоритм решения задачи можно представить в виде трех следующих рекурсивных подзадач:

1) перенести со стержня А N-1 дисков на вспомогательный стержень С;

перенести нижний диск со стержня А на стержень В;

перенести со стержня С N-1 дисков на стержень В.

Программа состоит из трех последовательно определяемых функций «ханойские-башни», «перенос», «выведи» и имеет вид:

Программа 130

(defun ханойские-башни (высота)

(рrоgn (перенос "а "Ь "с высота) "готово))

ХАНОЙСКИЕ-БАШНИ

(defun перенос (из в вспомогательный n)

(cond

((= п 1) ; ветвь 2

(выведи из в) (t (перенос из ; ветвь1 вспомогательный

в

(- n 1))

(выведи из в)

(перенос вспомогательный ; ветвь 3

в

из

(- п 1)))))

ПЕРЕНОС

(defun выведи (из в)

(format t "~S -> ~S~%"из в))

ВЫВЕДИ

 

Вызов функции «ханойские башни» дает такое решение:

 

(ханойские-башни 3)

А->В

А->С

В->C

А->В

С->А

С->В

А->В

ГОТОВО

 

Можно убедиться, что определенная нами функция дает правильное решение для произвольного числа дисков, однако время решения задачи с увеличением числа дисков быстро возрастает.

СВОЙСТВА СИМВОЛОВ

В Лиспе могут быть определены, так называемые, свойства символов. Список свойств имеет вид:

 


(имя_свойства1 значение1 имя_свойства2 значение2 . .. имя_свойстваN значениеN).

 

Присваивание нового свойства или изменение значения существующего осуществляется с помощью функции PUTPROP (или просто PUT):

 

(PUTPROP символ свойство значение).

 

Выяснить значение свойства, связанного с символом, можно с помощью функции GET:

 

(GET символ свойство).

 

С использованием этой функции можно также присваивать свойства символам:

 

(SETF (GET символ свойство) значение).

 

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