Работа со списками.
Управляющие конструкции Автолиспа - ветвление
Поскольку список - главное действующее лицо языка Лисп, следует тщательно рассмотреть набор функций по работе с ними.
До некоторой степени список аналогичен массиву в языках типа Паскаля. Там доступ к конкретному элементу списка решается просто - указывается его номер
: a[5]. В Лиспе все несколько сложнее.Функция ( CAR l ) возвращает первый элемент списка l.
Например, ( CAR ( LIST 10 20 ) ) вернет 10.
Если список l является описанием координат точки, то ( CAR l ) возвращает координату X.
Название функции идет от первой, 1958 года, реализации языка Лисп на древнем компьютере, в работе которого немаловажную роль играл адресный регистр памяти
(Contents 0f Address Register), сокращенно CAR. Дж. Маккарти назвал в честь этого регистра одну из функций языка Лисп.Функция ( CDR l ) возвращает все элементы списка l, кроме первого. Иначе говоря, у списка отрывается "голова", а возвращается остающийся "хвост", причем даже если этот "хвост" длиной в один атом, он все равно будет списком:
( CDR ( LIST 10 20 )
) возвращает ( 20 )Названа функция также в честь одного из регистров древнего компьютера - Contents of Decrement Register.
Функция CDR не годится для получения координаты Y точки - она возвращает список, а координата, конечно же, должна быть выражена атомом - вещественным числом. По-хорошему надо из списка, возвращаемого функцией CDR, выделить первый элемент, написав
( SETQ p ( LIST 10 20 )
) ; координаты точки p( SETQ y ( CAR ( CDR p )
) )Но такая запись выглядит весьма громоздко. Поэтому в Лиспе предусмотрена возможность использования вложенных функций CAR и CDR, которые будут называться соответственно CADR, CDAR, CAAR, CDDR и так далее (до четырех уровней вложенности). При этом (CADR l) эквивалента ( CAR ( CDR l ) ).
Последний элемент списка как атом возвращает функция ( LAST l ). В принципе ее можно использовать для получения координаты Y, но где гарантия, что в один прекрасный день пользователь не включит режим использования трехмерных точек? Тогда LAST станет возвращать уже координату Z и ваша программа тут же потеряет работоспособность.
И, наконец, самая общая функция выделения элементов из списка: ( NTH n l ), которая возвращает n-й элемент списка l. Название функции происходит от английского окончания порядковых числительных -th.
Нумерация элементов списка в функции NTH начинается с нуля!
Хуже того, в Лиспе нет единообразия в этом вопросе: часть функций все же уверена, что нумерация начинается с единицы. Такая путаница - один из наиболее крупных "проколов" Лиспа.
Итак, поскольку наиболее часто нам нужно выделить отдельные координаты точки, подытожим, как это легче всего сделать:
Таблица 5.1 - Получение координат точек.
Точка p |
|
Координата Х |
Координата Y |
( CAR p ) |
( CADR p ) |
( NTH 0 p ) |
( NTH 1 p ) |
Разбор точек на координаты используется, если необходимо рассчитать положение точки через приращения. Например, при отрисовке фаски нужно узнать координаты точки P2, зная точку P1:
Рисунок 5.1 - Отрисовка фаски.
Хотя угол здесь задан в явном виде, использование функции POLAR будет некорректным: мы не знаем расстояние P1P2.
Если его высчитывать как гипотенузу прямоугольного треугольника, точность вычисления квадратного корня окажется ограниченной и мы не попадем в точку P2. Если бы мы рисовали фаску вручную, то мы бы ввели приращения по осям в виде @3,3 (т.е. переместиться от точки P1 вправо на 3мм и верх на 3 мм). Сделаем то же самое в Автолиспе:
( SETQ p2 ( LIST ( + ( CAR p1 ) 3 )
( + ( CADR p1 ) 3 )
) )Такая запись является
стандартным способом расчета координат точек в приращениях.При использовании списков, особенно списков-данных, часто необходимо добавлять в имеющийся список новые и новые значения, как бы "приклеивая" их к его хвосту. Для этого предназначена функция
( APPEND l1 l2 ), которая добавляет в конец списка l1 список l2 и возвращает новый, удлиненный список. Список l1 от этого автоматически не меняется, нужно сохранять результат выполнения функции APPEND.Обратите внимание: для добавления в список атома его сначала нужно превратить список из одного элемента!
Пример: в переменной а хранится список вида ( 19 49 ). К нему нужно добавить число 20. Делается это так:
( SETQ a ( APPEND a ( LIST 20 )
) )Вся эта работа со списками нужна нам не сама по себе, а для решения той или иной задачи. А любая мало-мальски сложная задача, в свою очередь, требует для своего решения применения ветвлений и циклов. Поэтому сейчас мы начнем рассматривать управляющие конструкции Автолиспа - ветвления и циклы.
И ветвление, и цикл в обязательном порядке содержат проверку условия. В качестве условий в Лиспе используются логические функции, возвращающие T (true - истина) или NIL (ложь): "<", ">", "<=", ">=", "=", "/=". Обратите внимание: операция "не равно" записывается не так, как в большинстве других языков программирования!
Функции сравнения могут применяться к целым и вещественным числам, а также текстовым строкам, но не к спискам.
Примеры:
( = 2 2 )
возвращает T( = 2 5 )
возвращает NIL( = "ABC" "AB" )
возвращает NILЕсли мы сравниваем вещественные числа, то следует помнить об ограниченной точности вычислений с ними. Особенно это относится к тригонометрическим функциям. Поэтому крайне нежелательно сравнивать результат тригонометрической функции с константой, иначе возникают трудноуловимые казусы следующего вида:
( SETQ a1 ( SIN 0.0 )
)( SETQ a2 ( SIN ( * 2.0 PI )
) )Тогда
( = a1 a2 ) возвращает NIL, т.е. с точки зрения Лиспа , поскольку не равен точно нулю.Что же делать, если нужно сравнить два вещественных числа, когда хотя бы одно из них - результат вычисления тригонометрической или иной функции? (заметим, что аналогичная проблема в других языках программирования просто обходится молчанием). Создатели Лиспа ввели в язык специальную функцию сравнения с заданной точностью:
( EQUAL e1 e2 точность )
Здесь точность - число
(0.1, 0.01..), указывающее, сколько знаков поле запятой принимается во внимание при сравнении выражений e1 и e2. Поэтому функция ( EQUAL ( SIN 0 )( SIN ( * PI 2 ) ) 0.1 ) вернет T.Функции сравнения могут объединяться при помощи логических функций, образуя сложные условия. В Лиспе имеется богатый набор логических функций (заметно больший, чем в других языках), из которых реально достаточно знать следующие четыре:
Таблица 5.2 - Логические функции языка Автолисп.
X |
Y |
X AND Y |
X OR Y |
X XOR Y |
NOT X |
T |
NIL |
NIL |
T |
T |
NIL |
T |
T |
T |
T |
NIL |
NIL |
NIL |
T |
NIL |
T |
T |
T |
NIL |
NIL |
NIL |
NIL |
NIL |
T |
Запомнить эти функции легко:
AND - и X, и Y истинны, тогда X AND Y будет истинно;
OR - или X, или Y, или X и Y сразу истинны, тогда X OR Y будет истинно;
XOR - или X, или Y истинны по отдельности, но не оба сразу, тогда X XOR Y будет истинно;
NOT - просто "переворачивает", инвертирует значение, превращая T в NIL, а NIL в T.
Пример:
Если в переменных a, b, c хранятся длины сторон треугольника, то весьма желательно, чтобы они все были больше нуля. Тогда получим сложное условие:
( AND ( > a 0 )
( > b 0 ) ( > c 0 ) )Теперь мы готовы к рассмотрению функции IF, обеспечивающей ветвление в программе. Ее общий вид:
f1
[f2]
)
(сразу будем использовать предложенную Н. Виртом запись с отступами)
Здесь с - условие (простое или сложное):, f1 - функция, выполняемая, если условие истинно (часть "то"), а f2 - функция, выполняемая, когда условие ложно (часть "иначе"), причем квадратные скобки говорят о том, что часть "иначе" может отсутствовать.
Простейший пример:
( IF ( < a 0 )
( PROMPT "\nПеременная a меньше нуля" )
( PROMPT "\n"Переменная a больше или равна нулю" )
)
А как быть, если в случае выполнения (или невыполнения) условия нужно выполнить не одну, а сразу несколько функций? Ведь синтаксис функции IF разрешает записать только одну. Проблема решается так же, как в языке Паскаль: там используются операторные скобки begin..end, а в Лиспе - функция ( PROGN f1 f2 .. fn ). Она всего лишь объединяет функции f1 f2 .. fn в один блок, который можно подставить в функцию IF.
Например, мы решаем квадратное уравнение и в переменную d записали дискриминант. Теперь нужно посчитать действительные корни и вывести их на экран:
( IF ( >= d 0 )
( PROGN
( SETQ x1 ( / ( + ( * b -1 )
( SQRT d ) ) ( * 2 a ) )( SETQ x2 ( / ( - ( * b -1 )
( SQRT d ) ) ( * 2 a ) )( PROMPT "\nX1=" )
( PRINT x1 )
( PROMPT "\nX2=" )
( PRINT x2 )
)
; конец PROGN( PROMPT "\nДействительных корней нет" )
; "иначе")
Продолжая рассматривать параллели с Паскалем, обратимся к функции COND, обеспечивающей множественное ветвление аналогично паскалевскому оператору CASE. Ее общий вид:
( COND
( c1 f11 f12 ... f1n1 )
( c2 f21 f22 ... f2n1 )
...
( cm fm1 fm2 ... fmnm )
)
Здесь с1 ..cm - логические условия, fnm - функции, выполняемые при выполнении того или иного условия.
Внимание! Условия проверяются последовательно до первого истинного. Если истинно сразу несколько условий, то выполняются только функции, относящиеся к первому из них, а остальные условия даже не проверяются.
Основное назначение функции COND - обработка ввода пользователя, например, так:
( SETQ a ( GETINT "\n1 - фаска, 2 - галтель, 3 - выточка" )
)( COND
( ( = a 1 )
.... ); фаска( ( = a 2 )
.... ); галтель( ( = a 3 )
.... ); выточка)
А вот пример неправильного применения функции COND. Пусть мы хотим присвоить переменной flag значение NIL, если хотя бы одна из сторон треугольника a, b, c оказалась отрицательной. Неопытному программисту вместо очевидного
( SETQ flag ( NOT
( OR ( <= a 0 )
( <= b 0 ) ( <= c 0 ) ) ) )может прийти в голову следующая пагубная идея:
Программа выглядит работоспособной, но представим себе, что будет происходить при следующих исходных данных: a=10, b=-5, c=2. Функция COND проверит первое условие a>0, убедится в его истинности, установит переменную flag в T и остальные условия
вообще проверять не будет! Поэтому тот прискорбный факт, что сторона b меньше нуля, останется незамеченным.