7.6 Операторы условных переходов

Традиционная условная конструкция в Lisp’е это cond. Однако, if гораздо проще и очень похожа на условные конструкции в других языках программирования. Она сделана примитивом в Common Lisp’е. Common Lisp также предоставляет конструкции диспетчеризации (распределения) case и typecase, которые часто более удобны, чем cond.

[Специальный оператор] if test then [else]

Оператор if обозначает то же, что и конструкция if-then-else в большинстве других языках программирования. Сначала выполняется форма test. Если результат не равен nil, тогда выбирается форма then. Иначе выбирается форма else. Выбранная ранее форма выполняется, и if возвращает то, что вернула это форма.

(if test then else)  (cond (test then) (t else))

Но в некоторых ситуациях if оказывается более читабельным.

Форма else может быть опущена. В таком случае, если значение формы test является nil, тогда ничего не будет выполнено и возвращаемое значение формы if будет nil. Если в этой ситуации значение формы if важно, тогда в зависимости от контекста стилистически удобнее использовать форму and. Если значение не важно, тогда удобнее использовать конструкцию when.


[Макрос] when test {form}*

(when test form1 form2 ... ) сначала выполняет test. Если результат nil, тогда ничего не выполняется и возвращается nil. Иначе, последовательно выполняются формы form слева направо (как неявный progn), и возвращается значение последней формы.

(when p a b c)  (and p (progn a b c))
(when p a b c)  (cond (p a b c))
(when p a b c)  (if p (progn a b c) nil)
(when p a b c)  (unless (not p) a b c)

В целях хорошего стиля, when обычно используется для выполнения побочных эффектов при некоторых условиях, и значение when не используется. Если значение все-таки важно, тогда, может быть, стилистически функции and или if более подходят.


[Макрос] unless test {form}*

(unless test form1 form2 ... ) сначала выполняет test. Если результат не nil, тогда ничего не выполняется и возвращается nil. Иначе, последовательно выполняются формы form слева направо (как неявный progn), и возвращается значение последней формы.

(unless p a b c)  (cond ((not p) a b c))
(unless p a b c)  (if p nil (progn a b c))
(unless p a b c)  (when (not p) a b c)

В целях хорошего стиля, unless обычно используется для выполнения побочных эффектов при некоторых условиях, и значение unless не используется. Если значение все-таки важно, тогда может быть стилистически более подходящая функция if.


[Макрос] cond {(test {form}*)}*

Форма cond содержит некоторое (возможно нулевое) количество подвыражений, которые является списками форм. Каждое подвыражение содержит форму условия и ноль и более форм для выполнения. Например:

(cond (test-1 consequent-1-1 consequent-1-2 ...)
      (test-2)
      (test-3 consequent-3-1 ...)
      ... )

Отбирается первое подвыражение, чья форма условия вычисляется в не-nil. Все остальные подвыражения игнорируются. Формы отобранного подвыражения последовательно выполняются (как неявный progn).

Если быть точнее, cond обрабатывает свои подвыражения слева направо. Для каждого подвыражения, вычисляется форма условия. Если результат nil, cond переходит к следующему подвыражению. Если результат t, cdr подвыражения обрабатывается, как список форм. Этот список выполняется слева направо, как неявный progn. После выполнения списка форм, cond возвращает управление без обработки оставшихся подвыражений. Оператор cond возвращает результат выполнения последней формы из списка. Если этот список пустой, тогда возвращается значение формы условия. Если cond вернула управление без вычисления какой-либо ветки (все условные формы вычислялись в nil), возвращается значение nil.

Для того, чтобы выполнить последнее подвыражение, в случае если раньше ничего не выполнилось, можно использовать t для формы условия. В целях стиля, если значение cond будет для чего-то использоваться, желательно записывать последнее выражение так: (t nil). Также вопросом вкуса является запись последнего подвыражения cond как «синглтон», в таком случае, используется неявный t. (Следует отметить, если x может возвращать несколько значений, то (cond ... (x)) может вести себя отлично от (cond ... (t x)). Первое выражение всегда возвращает одно выражение, тогда как второе возвращает все то же, что и x. В зависимости от стиля, можно указывать поведение явно (cond ... (t (values x))), используя функцию values для явного указания возврата одного значения.) Например:

(setq z (cond (a ’foo) (b ’bar))) ;Возможна неопределённость
(setq z (cond (a ’foo) (b ’bar) (t nil))) ;Уже лучше
(cond (a b) (c d) (e)) ;Возможна неопределённость
(cond (a b) (c d) (t e)) ;Уже лучше
(cond (a b) (c d) (t (values e))) ;Неплохо (если необходимо
; одно значение)
(cond (a b) (c)) ;Возможна неопределённость
(cond (a b) (t c)) ;Уже лучше
(if a b c) ;Тоже неплохо

Lisp’овая форма cond сравнима с последовательностью if-then-else, используемой в большинстве алгебраических языках программирования:

(cond (p ...) if p тогда ...
      (q ...) roughly иначе если q тогда ...
      (r ...) corresponds иначе если r тогда ...
      ... to ...
      (t ...)) иначе ...


[Макрос] case keyform {({({key}*) | key} {form}*)}*

case условный оператор, который выбирает ветку для выполнения, в зависимости от равенства некоторой переменной некоторой константе. Константа обычно представляет собой ключевой символ, целое число или буква (но может быть и любой другой объект). Вот развёрнутая форма:

(case keyform
  (keylist-1 consequent-1-1 consequent-1-2 ...)
  (keylist-2 consequent-2-1 ...)
  (keylist-3 consequent-3-1 ...)
  ...)

Структурно case очень похож на cond, и поведение такое же: выбрать список форм и выполнить их. Однако case отличается механизмом выбора подвыражения.

Сперва case вычисляет форму keyform для получения объекта, который называется ключевой объект. Затем case рассматривает все подвыражения. Если ключевой объект присутствует в списке keylist (то есть, если ключевой объект равен eql хотя бы одному элементу из списка keylist), то список форм выбранного подвыражения вычисляется, как неявный progn. case возвращает то же, что и последняя форма списка (или nil если список форм был пустой). Если ни одно подвыражение не удовлетворило условию, то case возвращает nil.

Ключи в списке ключей keylist не выполняются. В данном списке должны быть указаны литеральные ключи. Если один ключ попадается более чем в одном подвыражении, это считается ошибкой. Следствием является то, что порядок этих подвыражений не влияет на поведение конструкции case.

Вместо keylist можно записать один из символов: t или otherwise. Подвыражение с одним из таких символов всегда удовлетворяет условию выбора. Такое подвыражение должно быть последним (это исключение из правила о произвольности положения подвыражений). Смотрите также ecase и ccase, каждая из которых предоставляет неявное otherwise подвыражение для сигнализирования об ошибке, если ни одно подвыражение не удовлетворило условию.

Если в подвыражении только один ключ, тогда этот ключ может быть записан вместо списка. Такой «синглтоновый ключ» не может быть nil (так как возникают конфликты с (), который означает список без ключей), t, otherwise или cons-ячейкой.


[Макрос] typecase keyform {(type {form}*)}*

typecase условный оператор, который выбирает подвыражение на основе типа объекта. Развёрнутая форма:

(typecase keyform
  (type-1 consequent-1-1 consequent-1-2 ...)
  (type-2 consequent-2-1 ...)
  (type-3 consequent-3-1 ...)
  ...)

Структура typecase похожа на cond или case. Поведение также схоже в том, что выбирается подвыражение в зависимости от условия. Различие заключается в механизме выбора подвыражения.

Сперва typecase вычисляет форму keyform для создания объекта, называемого ключевым объектом. Далее typecase друг за другом рассматривает каждое подвыражение. Форма type, которая встречается в каждом подвыражении, является спецификатором типа. Данный спецификатор не вычисляется, поэтому должен быть литеральным. Когда ключевой объект принадлежит некоторый типу, то выделенный список форм consequent выполняется последовательно (как неявный progn). typecase возвращает то, что вернула последняя форма из списка (или nil если список был пуст). Если не одно подвыражение не было выбрано, typecase возвращает nil.

Как и для case можно использовать t или otherwise на позиции типа для задания подвыражений, которые будут выполняться, только если не было выполнено других подвыражений. Смотрите также etypecase и ctypecase, каждая из которых предоставляет неявную ветку otherwise для сигнализирования об ошибке, что ни одно подвыражение не удовлетворило условию.

Допустимо указывать более одного подвыражение, тип условия которого уже является подтипом условия другого подвыражения. В таком случае будет выбрано первое встретившееся подвыражение. Таким образом в typecase, в отличие от case, порядок следования подвыражений влияет на поведение всей конструкции.

(typecase an-object
   (string ...) ;Подвыражение обрабатывает строки
   ((array t) ...) ;Подвыражение обрабатывает общие массивы
   ((array bit) ...) ;Подвыражение обрабатывает битовые массивы
   (array ...) ;Обрабатывает все остальные массивы
   ((or list number) ...) ;Подвыражение обрабатывает списки и числа
   (t ...)) ;Подвыражение обрабатывает все остальные объекты