Размещено на сайте пользователей САПР, здесь вы также можете скачать документацию по Автокад и Автолисп, а так же бесплатные Лисп программы для Автокад

ГЛАВА 6

Управляющие конструкции Автолиспа. Циклы

 

 Как мы узнали в первой главе, одной из трех базовых структур в любом языке программирования является цикл.

Нужно ясно понимать, что цикл выполняет двоякую функцию:

  1. Выполнение одного и того же участка программы более одного раза;
  2. Передача управления (под управлением понимается выполняемый в данный момент элемент программы) "назад" или "вверх" (т.е. ближе к началу программы).

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

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

Циклы в Лиспе, разумеется, обеспечиваются функциями. Простейшая из них - функция REPEAT вида

 

( REPEAT n f1 f2 ... fm )

 

Здесь n - число повторений, а f1..fm - те функции, которые будут выполняться n раз.

В цикле REPEAT, в отличие от, например, цикла FOR..TO в Паскале, нет переменной, значение которой изменяется от 1 до n. Поэтому таким циклом не стоит решать задачи перебора элементов списка. Для этого существуют специальные функции.

Назначение цикла REPEAT - в основном решение задач отрисовки. Например, если деталь имеет повторяющиеся элементы, можно запросить у пользователя их количество, затем вычленить повторяющийся участок и отрисовать его нужное число раз.

Не пытайтесь рисовать в цикле правильные многоугольники - для этого есть команда МН-УГОЛ.

Пример: нарисовать деталь, показанную на рисунке 6.1. Число отверстий, идущих с заданным шагом, может варьироваться от 1 до 5.

Рисунок 6.1 - Деталь, для отрисовки которой нужен цикл.

 

Предположим, что все данные, кроме числа отверстий, введены, сам контур детали нарисован и левый нижний угол находится в точке с координатами (0,0). Делаем следующее:

. . .

( INITGET 7 )

( SETQ n ( GETINT "\nВведите число отверстий:" ) x ( LIST s ( / h 2.0 ) ) )

( REPEAT n

( COMMAND "КРУГ" x ( / D 2.0 ) )

( SETQ x ( POLAR x 0 m ) )

)

 

В этом примере в переменную x записывается текущая координата центра окружности.

Однако одного такого цикла нам явно недостаточно. Как быть, если мы не знаем заранее, сколько раз нам нужно выполнять то или иное действие? Мы просто знаем, что нужно остановиться при выполнении определенного условия. Например, пусть в той же детали (рисунок 6.1) сверлятся отверстия, причем центр первого из них отстоит от левого края на величину S, а край последнего должен отстоять от правого края на величину B (рисунок 6.2).

Рисунок 6.2 - Использование цикла "пока".

Конечно, можно рассчитать число отверстий. А можно просто рисовать их до тех пор, пока соблюдается условие:

(6.1)

где x - текущая координата центра отверстия;

xR - координата правой границы детали.

 

В Лиспе цикл по условию задается функцией WHILE:

 

( WHILE c

f1 f2 ... fm )

 

Здесь c - логическое условие. Цикл выполняется, пока это условие истинно.

В нашем случае (p0 - координаты левого нижнего угла детали):

;координата х правого конца детали

( SETQ pr ( + ( CAR p0 ) L ) )

; Координата центра первого отверстия

( SETQ x ( + ( CAR p0 ) S ) )

( WHILE ( < ( - xr ( + x ( / D 2 ) ) ) B )

( COMMAND "CIRCLE"

( LIST x ( + ( CADR p0 ) H ) ) D )

( SETQ x ( + x m ) )

)

 

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

Цикл WHILE можно по ошибке сделать "вечным". Чтобы этого не происходило, в теле цикла должны обязательно изменяться переменные, входящие в условие цикла. При зацикливании иногда не удается прервать цикл нажатием клавиш Ctrl+C или Ctrl+Pause. Тогда придется перезагружать компьютер с потерей чертежа.

Еще одно частое применение цикла WHILE - тот самый возврат к началу программы. Например, нужно что-то нарисовать и спросить у пользователя "Повторить? Да/Нет". Самый простой способ сделать это - использовать переменную логического типа, в которую можно записать результат проверки условия и которую можно использовать в качестве условия цикла WHILE. Например:

; Задание начального условия

( SETQ flag T )

( WHILE flag

....

....

( SETQ ans ( GETSTRING "\nПовторить?<Y/N>" ) )

( SETQ flag ( = ( STRCASE ans ) "Y" ) )

); конец цикла

 

Работает этот фрагмент кода следующим образом: в качестве условия цикла задана переменная flag. Это значит, что цикл выполняется, если в эту переменную записано логическое значение "истина", т.е. T. Внутри цикла переменную flag нужно обязательно изменять, иначе произойдет зацикливание. Мы это делаем простейшим способом: вводим с клавиатуры букву ("Y" или "N"), переводим ее на верхний регистр (для удобства пользователя - он сможет вводить и заглавные, и строчные буквы; разумеется, с русскими буквами функция STRCASE не работает) и затем сравниваем со значением "Y". Результат любого сравнения - либо T (действительно введена буква "Y"), либо NIL (введена другая буква). Этот результат мы и записываем в переменную flag. Затем управление передается снова на начало цикла и делается проверка: если flag=T - цикл выполняется снова, а если flag=NIL - цикл прекращается.

Как уже отмечалось выше, в Лиспе нет цикла с переменной, которая меняется от min до max с заданным шагом. Между тем такие циклы есть практически во всех других языках программирования. В чем же дело? В том, что список - особая структура данных. Наиболее часто цикл с переменной нужен для прохода по всем элементам массива и их обработки (например, суммирования для нахождения среднего). В Лиспе для обработки каждого элемента списка предусмотрены два особых цикла: FOREACH и MAPCAR.

Нет необходимости искусственно (при помощи цикла WHILE) создавать цикл с переменной для прохода по списку!

Цикл FOREACH:

 

( FOREACH name list exp )

 

Здесь name - имя переменной, в которую последовательно записываются элементы списка list, а exp - выражение Лиспа, выполняемое столько раз, сколько элементов есть в списке list.

Рассмотрим работу цикла на примере. Пусть нам нужно просуммировать все элементы списка data и записать сумму в переменную s. Делается это так:

 

; Обнуление суммы

( SETQ s 0 )

( FOREACH cur data ( SETQ s ( + s cur ) ) )

 

При выполнении этого фрагмента происходит следующее: берется список data, по очереди каждый его элемент, начиная с первого, записывается в переменную cur, после чего выполняется выражение

 

( SETQ s ( + s cur ) )

 

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

Если нужно выполнить сложное действие над элементами списка, есть два пути: написать для этого отдельную функцию и вызвать ее из FOREACH, передавая ей текущий элемент списка как параметр, или объединить несколько выражений при помощи функции PROGN. Например, если нужно и сложить, и перемножить элементы списка, это можно сделать в одном цикле:

; Инициализация суммы и произведения

( SETQ s 0 p 1 )

( FOREACH cur data

( PROGN

( SETQ s ( + s cur ) )

( SETQ P ( * p cur ) )

)

)

 

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

Существует элегантный способ сделать это при помощи функции MAPCAR. К сожалению, в большинстве книг по Лиспу назначение функции MAPCAR или не разъясняется вовсе, или разъясняется неверно. Ее общий вид:

( MAPCAR 'f l1 l2 ... ln )

 MAPCAR выполняет функцию f поочередно над первыми, вторыми, третьими... элементами списков l1 .. ln. При этом n должно быть равно числу аргументов функции f, а все списки l1 ... ln должны содержать одинаковое число элементов. Самое главное - функция возвращает список результатов выполнения функции f.

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

Рассмотрим работу функции на примере. Итак, предположим, что все элементы списка размеров size нужно умножить на масштабный коэффициент k. Функция умножения требует как минимум два аргумента, в данном случае - текущий элемент списка и коэффициент k.

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

 

( SETQ coeff NIL )

( REPEAT ( LENGTH size )

( SETQ coeff ( APPEND coeff ( LIST k ) ) )

);REPEAT

 

После этого можно применить функцию MAPCAR:

 

( SETQ size ( MAPCAR '* size coeff ) )

 

Такое решение работоспособно, но неэлегантно: приходится создавать лишний список coeff. Чтобы этого избежать, можно было бы написать отдельную функцию умножения на коэффициент k. Но снова возникает проблема: как передать в нее значение k?

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

"Одноразовая" функция создается функцией Лиспа LAMBDA:

 

( LAMBDA ( arg1 ... argn ) exp1 ... expm )

 

Здесь arg1 ... argn - список аргументов "одноразовой" функции, а exp1 ... expm - выражения Лиспа, выполняющие какие-то операции над аргументами.

 Как и при определении функции при помощи DEFUN, "одноразовая" функция возвращает результат вычисления последнего выражения, т.е. expm.

Сама по себе функция LAMBDA возвращает создаваемую ей временную функцию. Поэтому ее можно прямо записать как аргумент функции MAPCAR, не забыв поставить апостроф для подавления ее выполнения:

  

( MAPCAR

'( LAMBDA ( x ) ( * x k ) )

size

)

 

Здесь определена функция с одним аргументом x, которая вычисляется для каждого элемента списка size. Полученные произведения "слепливаются" в список, являющийся возвращаемым значением функции MAPCAR.


Тесты к главе 6

Содержание

Список функций

Тесты

Предметный указатель