5.2 Функции

Существуют два метода указать функцию для использования в форме вызова функции. Один из них заключается в указании символа имени функции. Это использование символов для обозначения функций полностью независимо от их использования для обозначения специальных и лексических переменных. Другой путь заключается в использовании лямбда-выражения, которое является списком с первым элементом равным lambda. Лямбда-выражение не является формой, оно не может быть полноценно вычислено. Лямбда выражения и символы, когда они используются в программах для обозначения функций, могут быть указаны в качестве первого элемента формы вызова функции, или только в качестве второго параметры в операторе function. Следует отметить, что в этих двух контекстах символы и лямбда-выражения обрабатываются, как имена функций. Необходимо отличать это от обработки символов и лямбда выражений, как функциональных объектов, или объектов функций (function objects), которые удовлетворяют предикату functionp, как при представлении таких объектов в вызовы функций apply или funcall.

5.2.1 Именованные функции

Функция может иметь два типа имена. Глобальное имя может быть дано функции с помощью конструкции defun. Локальное имя может быть дано функции с помощью операторов flet или labels. Когда для функции задаётся имя, то с этим именем связывается лямбда-выражение с информацией о сущностях, которые были лексически доступны на момент связи. Если в качестве первого элементы формы вызова функции используется символ, тогда он ссылается на определение функции из наиболее ближней формы flet или labels, которые в своем тексте содержат эту форму, иначе символ ссылается на глобальное определение функции, при отсутствии вышеназванных форм.

5.2.2 Лямбда-выражения

Лямбда-выражение является списком со следующим синтаксисом:

(lambda lambda-list . body)

Первый элемент должен быть символом lambda. Второй элемент должен быть списком. Он называется лямбда-списком, и задаёт имена для параметров функции. Когда функция, обозначенная лямбда-выражением, применяется к аргументам, аргументы подставляются в соответствии с лямбда-списком. body может впоследствии ссылаться на аргументы используя имена параметров. body состоит из любого количества форм (возможно нулевого количества). Эти формы выполняются последовательно, и в качестве значения возвращается результат только последней формы (в случае отсутствия форм, возвращается nil). Полный синтаксис лямбда-выражения:

(lambda ( {var}*
          [&optional {var | (var [initform [svar]])}*]
          [&rest var]
          [&key {var | ({var | (keyword var)} [initform [svar]])}*              [&allow-other-keys]]
          [&aux {var | (var [initform])}*] )
    [[ {declaration}* | documentation-string]]
    {form}* )

Каждый элемент лямбда-списка является или спецификатором параметра или ключевым символом лямбда-списка. Ключевые символы лямбда-списка начинаются с символа &. Следует отметить, что ключевые символы лямбда-списка не является ключевыми символами в обычном понимании. Они не принадлежат пакету keyword. Они являются обычными символами, имена которых начинаются амперсандом. Такая терминология запутывает, но так сложилась история.

Keyword в предыдущем определении лябмда-списка может быть любым, а не только ключевым из пакета keyword, символом. Смотрите ниже.

Лямбда-список имеет пять частей, любая или все могут быть пустыми:

Когда функция, заданная лямбда-выражением, применяется к аргументам, то эти аргументы и параметры вычисляются слева направо. В простейшем случае, в лямбда-списке присутствуют только обязательные параметры. Каждый из них задаётся просто именем переменной var параметра. Когда функция применяется, аргументов должно быть столько же, сколько и параметров, и каждый параметр связывается с одним аргументом. В общем случае, каждый параметр связывается как лексическая переменная, если только с помощью декларации не указано, что связь должна осуществляться, как для специальной переменной. Смотрите defvar, proclaim, declare.

В более общем случае, если указано n обязательных параметров (n может равняться нулю), тогда должно быть как минимум n аргументов, и обязательные параметры будут связаны с n первыми аргументами.

Если указаны необязательные параметры, тогда каждый из них будет обработан так, как описано ниже. Если осталось некоторое количество аргументов, тогда переменная параметра var будет связана с оставшимся аргументом. Принцип такой же, как и для обязательных параметров. Если не осталось аргументов, тогда выполняется часть initform, и переменная параметра связывается с её результатом (или с nil, если форма initform не была задана). Если в спецификаторе указано имя ещё одной переменной svar, то она связывается с true, если аргумент был задан, и с false аргумент не был задан (и в таком случае выполнилась initform). Переменная svar называется supplied-p параметр. Она связывается не с аргументом, а со значением, которое показывает был ли задан аргумент для данного параметра или нет.

После того, как все необязательные параметры были обработаны, может быть указан оставшийся (rest) параметр. Если оставшийся (rest) параметр указан, он будет связан со списком все оставшихся необработанных аргументов. Если таких аргументов не осталось, оставшийся (rest) параметр будет связан с пустым списком. Если в лямбда списке отсутствуют оставшийся (rest) параметр и именованные (keyword) параметры, то необработанных аргументов оставаться не должно (иначе будет ошибка).

X3J13 voted in January 1989 to clarify that if a function has a rest parameter and is called using apply, then the list to which the rest parameter is bound is permitted, but not required, to share top-level list structure with the list that was the last argument to apply. Programmers should be careful about performing side effects on the top-level list structure of a rest parameter.

This was the result of a rather long discussion within X3J13 and the wider Lisp community. To set it in its historical context, I must remark that in Lisp Machine Lisp the list to which a rest parameter was bound had only dynamic extent; this in conjunction with the technique of “cdr-coding” permitted a clever stack-allocation technique with very low overhead. However, the early designers of Common Lisp, after a great deal of debate, concluded that it was dangerous for cons cells to have dynamic extent; as an example, the “obvious” definition of the function list

(defun list (&rest x) x)

could fail catastrophically. Therefore the first edition simply implied that the list for a rest parameter, like all other lists, would have indefinite extent. This still left open the flip side of the question, namely, Is the list for a rest parameter guaranteed fresh? This is the question addressed by the X3J13 vote. If it is always freshly consed, then it is permissible to destroy it, for example by giving it to nconc. However, the requirement always to cons fresh lists could impose an unacceptable overhead in many implementations. The clarification approved by X3J13 specifies that the programmer may not rely on the list being fresh; if the function was called using apply, there is no way to know where the list came from.

Далее обрабатываются все именованные (keyword) параметры. Для этих параметров обрабатываются те же аргументы, что и для оставшегося (rest) параметра. Безусловно, возможно указывать и &rest и &key. В таком случае оставшиеся аргументы используются для обеих целей: все оставшиеся аргументы составляются в список для &rest параметра и они также обрабатываются, как &key параметры. Только в этой ситуации один аргумент может обрабатываться более чем для одного параметра. Если указан &key, должно остаться чётное количество аргументов. Они будут обработаны попарно. Первый аргумент в паре должен быть ключевым символом, который задаёт имя параметра, второй аргумент должен быть соответствующим значением.

Именованный параметр в лямбда-списке может быть любым символом, а не только ключевым символом из пакета keyword. Если после &key используется только переменная или переменная в круглых скобках (возможно с первоначальным значением и svar), то поведение такое же как и было: для задания именованного параметра используется ключевой символ с тем же именем, что и переменная. Если спецификатор параметра выглядит так: ((keyword var) ...) то имя параметра может не быть ключевым символом из пакета keyword. Например:

(defun wager (&key ((secret password) nil) amount)
  (format nil "Вы ~A $~D"
          (if (eq password ’joe-sent-me) "выиграли" "проиграли")
          amount))

(wager :amount 100)  "Вы проиграли $100"
(wager :amount 100 ’secret ’joe-sent-me)  "Вы выиграли $100"

В данном примере слово secret может быть сделано более секретным, если поместить его в некоторый пакет obscure. Тогда для выигрыша можно будет использовать это так:

(wager :amount 100 ’obscure:secret ’joe-sent-me)  "Вы выиграли $100"

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

(defun foo (&key radix (type ’integer)) ...)

означает то же, что и

(defun foo (&key ((:radix radix)) ((:type type) ’integer)) ...)

Спецификатор именованного (keyword) параметра, как и все спецификаторы параметров, обрабатывается слева направо. Для каждого спецификатора именованного параметра, если в паре аргумента, в которой ключевой символ совпадает с именем параметра (сравнение производится с помощью eq), тогда переменная параметра связывается значением из этой пары. Если имеется более одной пар аргументов с одинаковым именем, то это не ошибка. В таком случае используется наиболее левая пара. Если пары аргументов не нашлось, тогда выполняется initform и переменная параметра связывается с этим значением (или с nil, если initform не задана). Переменная svar используется в тех же целях, что и для необязательных параметров. Она будет связана с истиной, если была необходимая пара аргументов, и иначе — с ложью.

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

В случае возникновения одного из этих условий, можно использовать именованные аргументы, которые не имеют соответствующих параметров (эти аргументы будут доступны, как оставшийся &rest параметр). Целью этого механизма является возможность объединять лямбда-списки разных функции без необходимости копировать все спецификаторы именованных (keyword) параметров. Например функция обёртка может передать часть именованных аргументов в обернутую функцию без необходимости явного ручного указания их всех.

После того как все спецификаторы были обработаны, слева направо обрабатываются спецификаторы вспомогательных параметров. Для каждого из них выполняется initform и переменная var связывается с этим результатом (или с nil, если initform не определена). С &aux переменными можно делать то же, что и со оператором let*:

(lambda (x y &aux (a (car x)) (b 2) c) ...)
    (lambda (x y) (let* ((a (car x)) (b 2) c) ...))

Что использовать зависит только от стиля.

Когда какая-либо форма initform выполняется в каком-либо спецификаторе параметра, данная форма может ссылаться на любую переменную параметра, стоящую слева от данной формы, включая supplied-p переменные, и может рассчитывать на то, что другие переменные параметров ещё не связаны (включая переменную данного параметра).

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

Примеры использования &optional и &rest параметров:

((lambda (a b) (+ a (* b 3))) 4 5)  19
((lambda (a &optional (b 2)) (+ a (* b 3))) 4 5)  19
((lambda (a &optional (b 2)) (+ a (* b 3))) 4)  10
((lambda (&optional (a 2 b) (c 3 d) &rest x) (list a b c d x)))
    (2 nil 3 nil nil)
((lambda (&optional (a 2 b) (c 3 d) &rest x) (list a b c d x))
 6)
    (6 t 3 nil nil)
((lambda (&optional (a 2 b) (c 3 d) &rest x) (list a b c d x))
 6 3)
    (6 t 3 t nil)
((lambda (&optional (a 2 b) (c 3 d) &rest x) (list a b c d x))
 6 3 8)
    (6 t 3 t (8))
((lambda (&optional (a 2 b) (c 3 d) &rest x) (list a b c d x))
 6 3 8 9 10 11)
    (6 t 3 t (8 9 10 11))

Примеры &key параметров:

((lambda (a b &key c d) (list a b c d)) 1 2)
    (1 2 nil nil)
((lambda (a b &key c d) (list a b c d)) 1 2 :c 6)
    (1 2 6 nil)
((lambda (a b &key c d) (list a b c d)) 1 2 :d 8)
    (1 2 nil 8)
((lambda (a b &key c d) (list a b c d)) 1 2 :c 6 :d 8)
    (1 2 6 8)
((lambda (a b &key c d) (list a b c d)) 1 2 :d 8 :c 6)
    (1 2 6 8)
((lambda (a b &key c d) (list a b c d)) :a 1 :d 8 :c 6)
    (:a 1 6 8)
((lambda (a b &key c d) (list a b c d)) :a :b :c :d)
    (:a :b :d nil)

Пример смешения всех:

((lambda (a &optional (b 3) &rest x &key c (d a))
   (list a b c d x))
 1)  (1 3 nil 1 ())

((lambda (a &optional (b 3) &rest x &key c (d a))
   (list a b c d x))
 1 2)  (1 2 nil 1 ())

((lambda (a &optional (b 3) &rest x &key c (d a))
   (list a b c d x))
 :c 7)  (:c 7 nil :c ())

((lambda (a &optional (b 3) &rest x &key c (d a))
   (list a b c d x))
 1 6 :c 7)  (1 6 7 1 (:c 7))

((lambda (a &optional (b 3) &rest x &key c (d a))
   (list a b c d x))
 1 6 :d 8)  (1 6 nil 8 (:d 8))

((lambda (a &optional (b 3) &rest x &key c (d a))
   (list a b c d x))
 1 6 :d 8 :c 9 :d 10)  (1 6 9 8 (:d 8 :c 9 :d 10))

В лямбда-выражении, если оно стоит на первом месте в списке формы вызова функции, допускаются все ключевые символы лямбда-списка, хотя они и не очень-то полезны в таком контексте. Гораздо полезнее их использовать в глобальных функциях, определённых с помощью defun.

Все символы, что начинаются на & обычно зарезервированы для использования в качестве ключевых символов лямбда-списка, и не должны использоваться для имён переменных. Реализации Common Lisp’а могут также предоставлять свои дополнительные ключевые символы лямбда-списка.

[Константа] lambda-list-keywords

Значение lambda-list-keywords является списком всех ключевых символов лямбда-списка, используемых в данной реализации, включая те, которые используются только в defmacro. Этот список должен содержать как минимум символы &optional, &rest, &key, &allow-other-keys, &aux, &body, &whole и &environment.


Вот пример использования &allow-other-keys и :allow-other-keys, рассматривающий функцию, которая принимает два своих именованных аргумента и также дополнительные именованные аргументы, которые затем передаются make-array:

(defun array-of-strings (str dims &rest keyword-pairs
                         &key (start 0) end &allow-other-keys)
  (apply #’make-array dims
         :initial-element (subseq str start end)
         :allow-other-keys t
         keyword-pairs))

Такая функция принимает строку и информацию о размерности и возвращает массив с заданной размерностью, каждый из элементов которого равен заданной строке. Именованные аргументы :start и :end, как обычно (смотрите главу 14), можно использовать для указания того, что должна использоваться подстрока. Кроме того, использование &allow-other-keys в лямбда списке указывает на то, что вызов этой функции может содержать дополнительные именованные аргументы. Для доступа к ним используется &rest аргумент. Эти дополнительные именованные аргументы передаются в make-array. make-array не принимает именованные аргументы :start и :end, и было бы ошибкой допустить их использование. Однако указание :allow-other-keys равное не-nil значению позволяет передавать любые другие именованные аргументы, включая :start и :end, и они были бы приняты и проигнорированы.

[Константа] lambda-parameters-limit

Значение lambda-parameters-limit является положительным целым, которое невключительно является верхней границей допустимого количества имён параметров, которые могут использоваться в лямбда-списке. Значение зависит от реализации, но не может быть менее 50. Разработчики поощряются за создание данной границы как можно большей без потери производительности. Смотрите call-arguments-list.