Skip to content
Find file
Fetching contributors…
Cannot retrieve contributors at this time
810 lines (670 sloc) 64.7 KB
\chapter{Функции}
\label{ch:05}
\thispagestyle{empty}
Кроме правил синтаксиса и семантики, основу всех программ на Lisp составляют следующие три
компонента~-- функции, переменные и макросы. Вы использовали их во время создания базы
данных в главе~\ref{ch:03}, но я опустил много подробностей о том, как они работают и как
их лучше всего использовать. Я посвящу следующие главы этим вопросам, начав с функций,
которые, так же как и их аналоги в других языках программирования, обеспечивают основные
возможности абстракции.
Большая часть самого Lisp состоит из функций. Более трёх четвертей имён, указанных в
стандарте, являются именами функций. Все базовые типы данных полностью определены в
терминах функций, работающих с ними. Даже мощная объектная система языка Lisp построена на
концептуальном развитии понятий функции и обобщённой функции, которые будут описаны в
главе~\ref{ch:16}.
В~конце концов, несмотря на важность макросов (\textit{The Lisp Way!}), вся реальная
функциональность обеспечивается функциями. Макросы выполняются во время компиляции и
создают код программы. После того как все макросы будут раскрыты, этот код полностью будет
состоять из обращения к функциям и специальным операторам. Я не упоминаю, что макросы
сами являются функциями, которые используются для генерации кода, а не для выполнения
действий в программе\pclfootnote{Несмотря на важность функций в Common Lisp, не совсем
правильно называть его \emph{функциональным} языком. Это правильно для некоторой части
возможностей Common Lisp, таких как функции работы со списками, которые созданы для
использования в стиле \texttt{body-form*}. Конечно же Lisp занимает значительное место
в истории функционального программирования~-- McCarthy ввёл в обращение много идей,
которые считаются очень важными для функционального программирования, но Common Lisp был
умышленно спроектирован для поддержки разных стилей программирования. В~семействе
Lisp-подобных языков язык Scheme является наиболее близким к понятию <<чистого>>
функционального языка, но даже он имеет несколько возможностей, которые отделяют его от
чистоты таких языков, как Haskell и ML.}.
\section{Определение новых функций}
Обычно функции определяются при помощи макроса \lstinline{DEFUN}. Типовое использование
\lstinline{DEFUN} выглядит вот так:
\begin{myverb}
(defun name (parameter*)
"Optional documentation string."
тело-функции*)
\end{myverb}
В~качестве имени может использоваться любой символ\pclfootnote{Хорошо, почти любой символ.
Неопределённым является поведение, когда вы в качестве имени для ваших функций
используете одно из имён, указанных в стандарте. Однако, как вы увидите в главе~\ref{ch:21},
система пакетов Lisp позволяет вам создавать имена в разных пространствах имён, так что
это не является проблемой.}. Как правило, имена функций содержат только буквы, цифры и
знак минус, но, кроме того, разрешено использование других символов, и они используются в
определённых случаях. Например, функции, которые преобразуют значения из одного типа в
другой, иногда используют символ \lstinline{->} в имени. Или функция, которая преобразует
строку в виджет, может быть названа \lstinline{string->widget}. Наиболее важное соглашение по
именованию, затронутое в главе~\ref{ch:02}, заключается в том, что лучше создавать составные имена,
используя знак минус вместо подчёркивания или заглавные буквы внутри имени.
Так что \lstinline{frob-widget} лучше соответствует стилю Lisp, чем \lstinline!frob_widget! или
\lstinline{frobWidget}.
Список параметров функции определяет переменные, которые будут использоваться для хранения
аргументов, переданных при вызове функции\pclfootnote{Списки параметров иногда называются
лямбда-списками из-за исторического отношения между понятием функции в Lisp и
лямбда-исчислением.}. Если функция не принимает аргументов, то список пуст и
записывается как \lstinline{()}. Различают обязательные, необязательные, множественные и
именованные (keyword) параметры. Эти вопросы будут обсуждаться подробнее в следующем
разделе.
За списком параметров может находиться строка, которая описывает назначение функции.
После того как функция определена, эта строка (строка документации) будет ассоциирована
с именем функции и может быть позже получена с помощью функции
\lstinline{DOCUMENTATION}\footnote{Например, следующий код:
\begin{myverb}
(documentation 'foo 'function)
\end{myverb}
\noindent{}вернёт строку документации для функции \lstinline{foo}. Однако заметьте, что документация
предназначается для людей, а не для работы программ. Реализации Lisp не обязаны сохранять
их и могут удалить их в любое время, так что переносимые программы не должны
зависеть от наличия документации к функции. В~некоторых реализациях требуется установка
специальных переменных, имена которых зависят от конкретной реализации, чтобы они начали
хранить документацию к функциям.}\hspace{\footnotenegspace}.
Тело \lstinline{DEFUN} состоит из любого числа выражений Lisp. При вызове функции они
вычисляются по порядку, и результат вычисления последнего выражения возвращается как
значение функции. Для возврата из любой точки функции может использоваться специальный
оператор \lstinline{RETURN-FROM}, что я продемонстрирую через некоторое время.
В~главе~\ref{ch:02} мы написали функцию \lstinline{hello-world}, которая выглядела вот так:
\begin{myverb}
(defun hello-world () (format t "hello, world"))
\end{myverb}
Теперь вы можете проанализировать части этой функции. Она называется \lstinline{hello-world},
список параметров пуст, потому что она не принимает аргументов, в ней нет строки
документации и её тело состоит из одного выражения:
\begin{myverb}
(format t "hello, world")
\end{myverb}
Вот пример немного более сложной функции:
\begin{myverb}
(defun verbose-sum (x y)
"Sum any two numbers after printing a message."
(format t "Summing ~d and ~d.~%" x y)
(+ x y))
\end{myverb}
Эта функция называется \lstinline{verbose-sum}, получает два аргумента, которые связываются с
параметрами \lstinline{x} и \lstinline{y}, имеет строку документации, и её тело состоит из двух
выражений. Значение, возвращённое вызовом функции \lstinline{+}, становится значением функции
\lstinline{verbose-sum}.
\section{Списки параметров функций}
Это всё, больше нечего сказать об именах функций или о строках документации. В~оставшейся
части книги мы будем описывать то, что можно написать в теле функции, поэтому мы остаёмся
наедине со списками параметров функций.
Основное назначение списков параметров~-- объявление переменных, которые будут
использоваться для хранения аргументов, переданных функции. Когда список параметров
является простым списком имён переменных, как в \lstinline{verbose-sum}, то параметры
называются \textit{обязательными}. Когда функция вызывается, она должна получить ровно по
одному аргументу для каждого из обязательных параметров. Каждый параметр связывается с
соответствующим аргументом. Если функция вызывается с меньшим или большим количеством
аргументов, чем требуется, то Lisp сообщит об ошибке.
Однако списки параметров в Common Lisp предоставляют более удобные способы отображения
аргументов функции в параметры функции. В~дополнение к обязательным параметрам функция
может иметь \textit{необязательные} параметры. Или функция может иметь один параметр,
который будет связан со списком, содержащим все дополнительные аргументы. И в заключение
аргументы могут быть связаны с параметрами путём использования \textit{ключевых слов}
(keywords), а не путём соответствия позиции параметра и аргумента в списке. Таким
образом, списки параметров Common Lisp предоставляют удобное решение для некоторых общих
задач кодирования.
\section{Необязательные параметры}
В~то время как многие функции, подобно \lstinline{verbose-sum}, нуждаются только в
обязательных параметрах, не все функции являются настолько простыми. Иногда функции
должны иметь параметр, который будет использоваться лишь при некоторых вызовах, поскольку
он имеет <<правильное>> значение по умолчанию. Таким примером может быть функция,
создающая структуру данных, которая будет при необходимости расти. Поскольку структура
данных может расти, то часто не имеет значения какой начальный размер она имеет. Но
пользователь функции, который имеет понятие о том, сколько данных будет помещено в
подобную структуру, может улучшить производительность программы путём указания начального
размера этой структуры. Однако большинство пользователей данной функции, скорее всего,
позволят выбрать наиболее подходящий размер автоматически. В~Common Lisp вы можете
предоставить этим пользователям одинаковые возможности с помощью необязательных
параметров; пользователи, которые не хотят устанавливать значение сами, получат разумное
значение по умолчанию, а остальные пользователи смогут подставить нужное
значение\footnote{В~языках, которые явно не поддерживают необязательных параметров,
программисты обычно находят методы их эмуляции. Одна из техник заключается в
использовании предопределённых значений <<no-value>>, которые пользователь может
передать, показывая, что он хочет использовать значение по умолчанию. В~языке C,
например, часто используют \lstinline{NULL} в качестве такого предопределённого
значения. Однако подобная договорённость между функцией и её пользователями является
лишь подпоркой~-- в некоторых функциях или для некоторых аргументов предопределённым
значением может быть \lstinline{NULL}, в то время как для других функций или для других
аргументов таким значением может быть $-1$ или некоторая другая предопределённая
константа (часто заданная с помощью \lstinline!#define!).}\hspace{\footnotenegspace}.
Для определения функции с необязательными параметрами после списка обязательных параметров
поместите символ \lstinline!&optional!, за которым перечислите имена необязательных
параметров. Простой пример использования выглядит так:
\begin{myverb}
(defun foo (a b &optional c d)
(list a b c d))
\end{myverb}
Когда функция будет вызвана, сначала аргументы связываются с обязательными параметрами.
После того как обязательные параметры получили переданные значения и остались ещё
аргументы, то они будут присвоены необязательным параметрам. Если аргументы закончатся до
того, как иссякнет список необязательных параметров, то оставшиеся параметры получат
значение \lstinline{NIL}. Таким образом, предыдущая функция будет выдавать следующие
результаты:
\begin{myverb}
(foo 1 2) ==> (1 2 NIL NIL)
(foo 1 2 3) ==> (1 2 3 NIL)
(foo 1 2 3 4) ==> (1 2 3 4)
\end{myverb}
Lisp все равно будет проверять количество аргументов, переданных функции (в нашем случае
это число от 2 до 4 включительно), и будет выдавать ошибку, если функция вызвана с
лишними аргументами, или их, наоборот, недостаёт.
Конечно, вы можете захотеть использовать другие значения по умолчанию, отличные от
\lstinline{NIL}. Вы можете указать их путём замены имени параметра на список, состоящий из
имени и выражения. Это выражение будет вычислено, только если пользователь не указал
значения для необязательного параметра. Общепринятым является простое задание конкретного
значения в качестве выражения.
\begin{myverb}
(defun foo (a &optional (b 10))
(list a b))
\end{myverb}
Эта функция требует указания одного аргумента, который будет присвоен параметру \lstinline{a}.
Второй параметр~-- \lstinline{b} получит либо значение второго аргумента, если он указан, либо
число~10.
\begin{myverb}
(foo 1 2) ==> (1 2)
(foo 1) ==> (1 10)
\end{myverb}
Однако иногда вам потребуется большая гибкость в выборе значения по умолчанию. Вы
можете захотеть вычислять значение по умолчанию, основываясь на других параметрах. И вы
можете сделать это~-- выражение для значения по умолчанию может ссылаться на параметры,
ранее перечисленные в списке параметров. Если вы пишете функцию, которая возвращает
что-то типа описания прямоугольников, и вы хотите сделать её удобной для использования с
квадратами, то можете использовать такой вот список параметров:
\begin{myverb}
(defun make-rectangle (width &optional (height width))
...)
\end{myverb}
\noindent{}что сделает параметр \lstinline{height} равным параметру \lstinline{width}, если только он не будет
явно задан.
Иногда полезно будет знать, было значение необязательного параметра задано
пользователем или использовалось значение по умолчанию. Вместо того чтобы писать код,
который проверяет, является ли переданное значение равным значению по умолчанию (это все
равно не будет работать, поскольку пользователь может явно задать значение, равное
значению по умолчанию), вы можете добавить ещё одно имя переменной к списку параметров
после выражения для значения по умолчанию. Указанная переменная будет иметь истинное
значение, если пользователь задал значение для аргумента, и \lstinline{NIL} в противном случае.
По соглашению эти переменные называются так же, как и параметры, но с добавлением
<<\lstinline{-supplied-p}>> к концу имени. Например:
\begin{myverb}
(defun foo (a b &optional (c 3 c-supplied-p))
(list a b c c-supplied-p))
\end{myverb}
Выполнение этого кода приведёт к следующим результатам:
\begin{myverb}
(foo 1 2) ==> (1 2 3 NIL)
(foo 1 2 3) ==> (1 2 3 T)
(foo 1 2 4) ==> (1 2 4 T)
\end{myverb}
\section{Остаточные (rest) параметры}
Необязательные параметры применяются только тогда, когда у вас есть отдельные параметры,
для которых пользователь может указывать или не указывать значения. Но некоторые функции
могут требовать изменяемого количества аргументов. Некоторые встроенные функции, которые
вы уже видели, работают именно так. Функция \lstinline{FORMAT} имеет два обязательных
аргумента~-- поток вывода и управляющую строку. Но, кроме этого, он требует переменное
количество аргументов, зависящее от того, сколько значений он должен вставить в
управляющую строку. Функция \lstinline{+} также получает переменное количество аргументов~--
нет никаких причин ограничиваться складыванием только двух чисел, эта функция может
вычислять сумму любого количества значений. (Она даже может работать вообще без
аргументов, возвращая значение \lstinline{0}.) Следующие примеры являются допустимыми вызовами
этих двух функций:
\begin{myverb}
(format t "hello, world")
(format t "hello, ~a" name)
(format t "x: ~d y: ~d" x y)
(+)
(+ 1)
(+ 1 2)
(+ 1 2 3)
\end{myverb}
Очевидно, что вы можете написать функцию с переменным числом аргументов, просто описывая
множество необязательных параметров. Но это будет невероятно мучительно~-- простое
написание списка параметров может быть не очень хорошим делом, и это не связывает все
параметры с их использованием в теле функции. Для того чтобы сделать это правильно, вы
должны иметь число необязательных параметров равным максимальному допустимому количеству
аргументов при вызове функций. Это число зависит от реализации, но гарантируется, что оно
будет равно минимум \lstinline{50}. В~текущих реализациях оно варьируется от \lstinline{4,096} до
\lstinline{536,870,911}\footnote{Для вашей реализации вы можете узнать это значение, используя
константу \lstinline{CALL-ARGUMENTS-LIMIT}.}\hspace{\footnotenegspace}. Хех! Этот мозгодробительный подход явно не
является хорошим стилем написания программ.
Вместо этого Lisp позволяет вам указать параметр, который примет все аргументы (этот
параметр указывается после символа \lstinline!&rest!). Если функция имеет параметр
\lstinline!&rest! (остаточный параметр), то любые аргументы, оставшиеся после связывания
обязательных и необязательных параметров, будут собраны в список, который станет значением
остаточного параметра \lstinline!&rest!. Таким образом, список параметров для функций
\lstinline{FORMAT} и \lstinline{+} будет выглядеть примерно так:
\begin{myverb}
(defun format (stream string &rest values) ...)
(defun + (&rest numbers) ...)
\end{myverb}
\section{Именованные параметры}
Необязательные и остаточные (rest) параметры дают вам достаточно гибкости, но ни один из
них не помогает вам в следующей ситуации: предположим, что вы имеете функцию, которая
получает четыре необязательных параметра. Теперь предположим, что пользователь захочет
задать значение только для одного из параметров и даже что пользователь захочет задать
значение только для некоторых, расположенных не последовательно параметров.
Пользователи, которые хотят задать значение для первого параметра, не имеют никаких
проблем~-- они просто передадут один необязательный параметр и пропустят оставшиеся. Но
что делать пользователям, которые хотят указать значения для других параметров,~-- разве
это не та проблема, которую должно решить использование необязательных параметров?
Конечно, это она. Но проблема заключается в том, что необязательные параметры все равно
являются позиционными~-- если пользователь хочет указать четвёртый необязательный
параметр, то первые три необязательных параметра превращаются для этого пользователя в
обязательные. К счастью, существует ещё один вид параметров~-- именованные (keyword)
параметры, которые позволяют указывать пользователю, какие значения будут связаны с
конкретными параметрами.
Для того чтобы задать именованные параметры, необходимо после всех требуемых,
необязательных и остаточных параметров указать символ \lstinline!&key! и затем
перечислить любое количество спецификаторов именованных параметров. Вот пример функции,
которая имеет только именованные параметры:
\begin{myverb}
(defun foo (&key a b c)
(list a b c))
\end{myverb}
Когда функция вызывается, каждый именованный параметр связывается со значением, которое
указано после ключевого слова, имеющего то же имя, что и параметр. Вернёмся к
главе~\ref{ch:04}, в которой указывалось, что ключевые слова~-- это имена, которые
начинаются с двоеточия и которые автоматически определяются как константы, вычисляемые
сами в себя (self-evaluating).
Если ключевое слово не указано в списке аргументов, то соответствующий параметр получает
значение по умолчанию, т.~е. принцип тот же, что и для необязательных параметров.
Поскольку именованные аргументы имеют метку, то они могут быть указаны в любом порядке,
если они следуют после обязательных аргументов. Например, \lstinline{foo} может быть вызвана
вот так:
\begin{myverb}
(foo) ==> (NIL NIL NIL)
(foo :a 1) ==> (1 NIL NIL)
(foo :b 1) ==> (NIL 1 NIL)
(foo :c 1) ==> (NIL NIL 1)
(foo :a 1 :c 3) ==> (1 NIL 3)
(foo :a 1 :b 2 :c 3) ==> (1 2 3)
(foo :a 1 :c 3 :b 2) ==> (1 2 3)
\end{myverb}
Так же как и для необязательных параметров, для именованных параметров можно задавать
выражение для вычисления значения по умолчанию и имя \lstinline{supplied-p}-переменной. И для
необязательных, и для именованных параметров значение по умолчанию может ссылаться на
параметры, указанные ранее в списке.
\begin{myverb}
(defun foo (&key (a 0) (b 0 b-supplied-p) (c (+ a b)))
(list a b c b-supplied-p))
\end{myverb}
\begin{myverb}
(foo :a 1) ==> (1 0 1 NIL)
(foo :b 1) ==> (0 1 1 T)
(foo :b 1 :c 4) ==> (0 1 4 T)
(foo :a 2 :b 1 :c 4) ==> (2 1 4 T)
\end{myverb}
Так же, если по некоторым причинам вы хотите, чтобы пользователь использовал имена
аргументов, отличающиеся от имён параметров, то вы можете заменить имя параметра на
список, содержащий имя, которое будет использоваться пользователем при вызове, и имя
параметра. Следующее определение \lstinline{foo}:
\begin{myverb}
(defun foo (&key ((:apple a)) ((:box b) 0) ((:charlie c) 0 c-supplied-p)
(list a b c c-supplied-p))
\end{myverb}
\noindent{}позволяет пользователю вызывать функцию вот так:
\begin{myverb}
(foo :apple 10 :box 20 :charlie 30) ==> (10 20 30 T)
\end{myverb}
Этот стиль особенно полезен, если вы хотите полностью отделить публичный интерфейс от
деталей внутренней реализации, поскольку обычно внутри вы хотите использовать короткие
имена переменных и значащие имена в программном интерфейсе. Однако обычно это
используется не особо часто.
\section{Совместное использование разных типов параметров}
Использование всех четырёх типов параметров в одной функции хотя и является вполне
возможным, но применяется редко. Когда используется более одного типа параметров, они
должны быть объявлены в порядке, который мы уже обсуждали: сначала указываются имена
требуемых параметров, затем~-- необязательных, потом~-- остаточных (\lstinline!&rest!), и в
заключение~-- именованных параметров. Но обычно в функциях, которые используют несколько
типов параметров, комбинируют требуемые параметры с одним из других видов параметров или
комбинируют необязательные и остаточные параметры. Два других сочетания~--
необязательных или остаточных параметров с именованными параметрами~-- могут привести к
очень удивительному поведению функции.
Комбинация необязательных и именованных параметров преподносит достаточно сюрпризов, так
что вы, скорее всего, должны избегать их совместного использования. Проблема заключается в
том, что если пользователь не задаёт значений для всех необязательных параметров, то эти
параметры получают имена и значения именованных параметров. Например, эта функция
использует необязательные и именованные параметры:
\begin{myverb}
(defun foo (x &optional y &key z)
(list x y z))
\end{myverb}
Если она вызывается вот так, то все нормально:
\begin{myverb}
(foo 1 2 :z 3) ==> (1 2 3)
\end{myverb}
И вот все работает нормально:
\begin{myverb}
(foo 1) ==> (1 nil nil)
\end{myverb}
Но в этом случае она выдаёт ошибку:
\begin{myverb}
(foo 1 :z 3) ==> ERROR
\end{myverb}
Это происходит потому, что имя параметра \lstinline{:z} берётся как значение для
необязательного параметра \lstinline{y}, оставляя для обработки только аргумент \lstinline{3}. При
этом Lisp ожидает, что в этом месте встретится либо пара имя/значение, либо не будет
ничего, и одиночное значение приведёт к выдаче ошибки. Будет даже хуже, если функция
будет иметь два необязательных параметра, так что использование функции как в последнем
примере приведёт к тому, что значения \lstinline{:z} и \lstinline{3} будут присвоены двум
необязательным параметрам, а именованный параметр \lstinline{z} получит значение по
умолчанию~-- \lstinline{NIL}, без всякого указания, что что-то произошло неправильно.
В~общем, если вы обнаружите, что вы пишете функцию, которая использует и необязательные, и
именованные параметры, то вам лучше просто исправить её для использования только
именованных параметров~-- этот подход более гибок, и вы всегда сможете добавить новые
параметры, не беспокоя пользователей вашей функции. Вы можете даже удалять именованные
параметры, если никто не использует их\footnote{Четыре стандартные функции принимают
необязательные и именованные аргументы~-- \lstinline{READ-FROM-STRING},
\lstinline{PARSE-NAMESTRING}, \lstinline{WRITE-LINE} и \lstinline{WRITE-STRING}. Их оставили во время
стандартизации для обеспечения обратной совместимости с более ранними диалектами Lisp.
\lstinline{READ-FROM-STRING} является лидером по количеству ошибок, сделанных начинающими
программистами на Lisp,~-- вызов этой функции как \lstinline{(read-from-string s :start 10)}
игнорирует ключевое слово \lstinline{:start}, читает с индекса \lstinline{0}, а не с \lstinline{10}.
Это происходит, поскольку \lstinline{READ-FROM-STRING} имеет два необязательных параметра,
которые съедают аргументы \lstinline{:start} и \lstinline{10}.}\hspace{\footnotenegspace}. Использование именованных
параметров помогает сделать код более лёгким для сопровождения и развития~-- если вам
нужно изменить поведение функции, и это изменение потребует ввода новых параметров, вы
можете добавить именованные параметры без изменения или даже без перекомпиляции кода,
который использует эту функцию.
Вы можете безопасно комбинировать остаточные и именованные параметры, но вначале поведение
может показаться немного удивительным. Обычно наличие либо остаточных, либо именованных
параметров приведёт к тому, что значения, оставшиеся после заполнения всех обязательных и
необязательных параметров, будут обработаны определённым образом~-- либо собраны в список
(для остаточных параметров), либо присвоены соответствующим именованным параметрам. Если в
списке параметров используются и остаточные, и именованные параметры, то выполняются оба
действия~-- все оставшиеся значения собираются в список, который присваивается параметру
\lstinline!&rest!, а также соответствующие значения присваиваются именованным параметрам.
Так что, имея следующую функцию:
\begin{myverb}
(defun foo (&rest rest &key a b c)
(list rest a b c))
\end{myverb}
\noindent{}вы получите следующие результаты:
\begin{myverb}
(foo :a 1 :b 2 :c 3) ==> ((:A 1 :B 2 :C 3) 1 2 3)
\end{myverb}
\section{Возврат значений из функции}
Все функции, которые уже были написаны, так или иначе использовали обычное поведение,
заключающееся в возврате значения последнего вычисленного выражения как собственного
возвращаемого значения. Это самый употребительный способ возврата значений из функции.
Однако иногда бывает нужно вернуть значение из середины функции, вырываясь таким образом
из вложенных управляющих конструкций. В~таком случае вы можете использовать специальный
оператор \lstinline{RETURN-FROM}, который предназначен для немедленного возвращения любого
значения из функции.
Вы увидите в главе~\ref{ch:20}, что \lstinline{RETURN-FROM} в самом деле не привязана к функциям; она
используется для возврата из блока кода, определённого с помощью оператора \lstinline{BLOCK}.
Однако \lstinline{DEFUN} автоматически помещает тело функции в блок кода с тем же именем, что и
имя функции. Так что вычисление \lstinline{RETURN-FROM} с именем функции и значением, которое вы
хотите возвратить, приведёт к немедленному выходу из функции с возвратом указанного
значения. \lstinline{RETURN-FROM} является специальным оператором, чьим первым аргументом
является имя блока, из которого необходимо выполнить возврат. Это имя не вычисляется, так
что нет нужды его экранировать.
Следующая функция использует вложенные циклы для нахождения первой пары чисел, каждое из
которых меньше, чем \lstinline{10}, и чьё произведение больше заданного аргумента, и она использует
\lstinline{RETURN-FROM} для возврата первой найденной пары чисел:
\begin{myverb}
(defun foo (n)
(dotimes (i 10)
(dotimes (j 10)
(when (> (* i j) n)
(return-from foo (list i j))))))
\end{myverb}
Надо отметить, что необходимость указания имени функции, из которой вы хотите вернуться,
является не особо удобной~-- если вы измените имя функции, то вам нужно будет также
изменить имя, использованное в операторе \lstinline{RETURN-FROM}\footnote{Другой макрос,
\lstinline{RETURN}, не требует указания имени блока. Однако вы не можете использовать его
вместо \lstinline{RETURN-FROM}, для того чтобы не указывать имя функции,~-- это лишь
синтаксическая обвязка для возврата из блока с именем \lstinline{NIL}. Описание этого
макроса вместе с описанием \lstinline{BLOCK} и \lstinline{RETURN-FROM} будет сделано в
главе~\ref{ch:20}.}\hspace{\footnotenegspace}. Но следует отметить, что явное использование
\lstinline{RETURN-FROM} в Lisp происходит значительно реже, чем использование
выражения \lstinline{return} в C-подобных языках, поскольку
все выражения Lisp, включая управляющие конструкции, такие как условные выражения и циклы,
вычисляются в значения. Так что это не представляет особой сложности на практике.
\section{Функции как данные, или функции высшего порядка}
В~то время как основной способ использования функций~-- это вызов их с указанием имени,
существуют ситуации, когда было бы полезно рассматривать функции как данные. Например, вы
можете передать одну функцию в качестве аргумента другой функции, вы можете написать общую
функцию сортировки и предоставить пользователю возможность указания функции для сравнения
двух элементов. Так что один и тот же алгоритм может использоваться с разными функциями
сравнения. Аналогично обратные вызовы (callbacks) и ловушки (hooks) зависят от
возможности хранения ссылок на исполняемый код, который можно выполнить позже. Поскольку
функции уже являются стандартным способом представления частей кода, имеет смысл разрешить
рассмотрение функций как данных\pclfootnote{Конечно, Lisp не является единственным языком,
который позволяет рассматривать функции как данные. Язык C использует указатели на
функции, Perl использует ссылки на подпрограммы, Python использует подход, аналогичный
Lisp, а C\# ввёл делегаты (типизированные указатели на функции, призванные улучшить
механизмы, используемые в Java) и также механизм анонимных классов.}.
В~Lisp функции являются просто другим типом объектов. Когда вы определяете функцию с
помощью \lstinline{DEFUN}, вы в действительности делаете две вещи: создаёте новый
объект-функцию и даёте ему имя. Кроме того, имеется возможность, как вы увидели в
главе~\ref{ch:03}, использовать \lstinline{LAMBDA} для создания функции без имени.
Действительное представление объекта-функции, независимо от того, именованный он или нет,
является неопределённым~-- в компилируемых вариантах Lisp они, вероятно, состоят в
основном из машинного кода. Единственные вещи, которые вам надо знать,~-- как получить
эти объекты и как выполнять их, если вы их получили.
Специальный оператор \lstinline{FUNCTION} обеспечивает механизм получения объекта-функции. Он
принимает единственный аргумент и возвращает функцию с этим именем. Имя не экранируется.
Так что если вы определили функцию \lstinline{foo}, например, вот так:
\begin{myverb}
CL-USER> (defun foo (x) (* 2 x))
FOO
\end{myverb}
\noindent{}вы можете получить объект-функцию следующим образом\pclfootnote{Точное печатное
представление объекта-функции может отличаться в зависимости от реализации.}:
\begin{myverb}
CL-USER> (function foo)
#<Interpreted Function FOO>
\end{myverb}
В~действительности вы уже использовали \lstinline{FUNCTION}, но это было
замаскировано. Синтаксис \lstinline!#'!, который вы использовали в
главе~\ref{ch:03}, является синтаксической обёрткой для \lstinline{FUNCTION}, точно
так же как и \lstinline{'} является обёрткой для \lstinline{QUOTE}\footnote{Лучше всего
рассматривать \lstinline{FUNCTION} как специальный вид экранирования.
Экранирование символа предотвращает его вычисление, оставляя сам символ, а не
значение переменной с именем символа. \lstinline{FUNCTION} также изменяет
нормальные правила вычисления, но вместо предотвращения вычисления символа
заставляет вычислять его как имя функции, точно так же, как если бы этот символ
использовался в качестве имени функции в выражении вызова.}\hspace{\footnotenegspace}. Так что вы можете
получить объект-функцию вот так:
\begin{myverb}
CL-USER> #'foo
#<Interpreted Function FOO>
\end{myverb}
После того как вы получили объект-функцию, есть только одна вещь, которую вы можете
сделать с ней,~-- выполнить её. Common Lisp предоставляет две функции для выполнения
функции через объект-функцию: \lstinline{FUNCALL} и \lstinline{APPLY}\footnote{В~действительности
существует и третья возможность, специальный оператор \lstinline{MULTIPLE-VALUE-CALL}, но я
отложу этот вопрос до того момента, когда мы будем обсуждать выражения, возвращающие
множественные значения, в главе~\ref{ch:20}.}\hspace{\footnotenegspace}. Они отличаются тем, как они получают
аргументы, которые будут переданы вызываемой функции.
\lstinline{FUNCALL}~-- это функция, которая используется тогда, когда во время написания кода вы
знаете количество аргументов, которые вы будете передавать функции. Первым аргументом
\lstinline{FUNCALL} является запускаемый объект-функция, а оставшиеся аргументы передаются
данной функции. Так что следующие два выражения являются эквивалентными:
\begin{myverb}
(foo 1 2 3) === (funcall #'foo 1 2 3)
\end{myverb}
Однако довольно мало смысла в использовании \lstinline{FUNCALL} для вызова функции, чьё имя вы
знаете во время написания кода. В~действительности два предыдущих выражения, скорее всего,
скомпилируются в один и тот же машинный код.
Следующая функция демонстрирует более реалистичное использование \lstinline{FUNCALL}. Она
принимает объект-функцию в качестве аргумента и рисует простую текстовую диаграмму
значений, возвращённых функцией, вызываемой для значений от \lstinline{min} до \lstinline{max} с
шагом \lstinline{step}.
\begin{myverb}
(defun plot (fn min max step)
(loop for i from min to max by step do
(loop repeat (funcall fn i) do (format t "*"))
(format t "~%")))
\end{myverb}
Выражение \lstinline{FUNCALL} вычисляет значение функции для каждого значения \lstinline{i}.
Внутрений цикл использует это значение для определения того, сколько раз напечатать знак
<<звёздочка>>.
Заметьте, что вы не используете \lstinline{FUNCTION} или \lstinline!#'! для получения значения
\lstinline{fn}; вы хотите, чтобы оно интерпретировалось как переменная, поскольку значение этой
переменной является объектом-функцией. Вы можете вызвать \lstinline{plot} с любой функцией,
которая берёт один числовой аргумент, например со встроенной функцией \lstinline{EXP}, которая
возвращает значение \lstinline{e}, возведённое в степень переданного аргумента.
\begin{myverb}
CL-USER> (plot #'exp 0 4 1/2)
*
*
**
****
*******
************
********************
*********************************
******************************************************
NIL
\end{myverb}
Однако \lstinline{FUNCALL} не особо полезен, когда список аргументов становится известен только
во время выполнения. Например, для работы с функцией \lstinline{plot} в других случаях
представьте, что вы получили список, содержащий объект-функцию, минимальное и максимальное
значения, а также шаг изменения значений. Другими словами, список содержит значения,
которые вы хотите передать как аргументы для \lstinline{plot}. Предположим, что этот список
находится в переменной \lstinline{plot-data}. Вы можете вызвать \lstinline{plot} с этими значениями
вот так:
\begin{myverb}
(plot
(first plot-data)
(second plot-data)
(third plot-data)
(fourth plot-data))
\end{myverb}
Это работает нормально, но достаточно раздражает необходимость явного доставания
аргументов лишь для того, чтобы передать их функции \lstinline{plot}.
Это как раз тот случай, когда на помощь приходит \lstinline{APPLY}. Подобно \lstinline{FUNCALL}, её
первым аргументом является объект-функция. Но после первого аргумента, вместо
перечисления отдельных аргументов, она принимает список. Затем \lstinline{APPLY} применяет
функцию к значениям в списке. Это позволяет вам переписать предыдущий код следующим
образом:
\begin{myverb}
(apply #'plot plot-data)
\end{myverb}
Кроме того, \lstinline{APPLY} может также принимать <<свободные>> аргументы, так же как и обычные
аргументы в списке. Таким образом, если \lstinline{plot-data} содержит только значения для
\lstinline{min}, \lstinline{max} и \lstinline{step}, то вы все равно можете использовать \lstinline{APPLY} для
отображения функции \lstinline{EXP}, используя следующее выражение:
\begin{myverb}
(apply #'plot #'exp plot-data)
\end{myverb}
\lstinline{APPLY} не заботится о том, использует ли функция необязательные, остаточные или
именованные объекты,~-- список аргументов создаётся путём объединения всех аргументов, и
результирующий список должен быть правильным списком аргументов для функции с достаточным
количеством аргументов для обязательных параметров и соответствующими именованными
параметрами.
\section{Анонимные функции}
После того как вы начали писать или даже просто использовать функции, которые принимают в
качестве аргументов другие функции, вы, скорее всего, открыли для себя тот момент, что
иногда раздражает необходимость определять и давать имя функции, которая будет
использоваться в одном месте, особенно если вы никогда не будете вызывать её по имени.
Когда кажется, что определение новых функций с помощью \lstinline{DEFUN} является излишним, вы
можете создать <<анонимную>> функцию, используя выражение \lstinline{LAMBDA}. Как обсуждалось в
главе~\ref{ch:03}, \lstinline{LAMBDA}-выражение выглядит примерно так:
\begin{myverb}
(lambda (parameters) body)
\end{myverb}
Можно представить себе, что \lstinline{LAMBDA}-выражения~-- это специальный вид имён функций,
где само имя напрямую описывает, что эта функция делает. Это объясняет, почему вы можете
использовать \lstinline{LAMBDA}-выражение вместо имени функции с \lstinline!#'!.
\begin{myverb}
(funcall #'(lambda (x y) (+ x y)) 2 3) ==> 5
\end{myverb}
Вы даже можете использовать \lstinline{LAMBDA}-выражение как <<имя>> функции в выражениях,
вызывающих функцию. Если вы хотите, то вы можете переписать предыдущий пример с
\lstinline{FUNCALL} в следующем виде:
\begin{myverb}
((lambda (x y) (+ x y) 2 3) ==> 5
\end{myverb}
Но обычно так никогда не пишут, это использовалось лишь для демонстрации, что
\lstinline{LAMBDA}-выражения разрешено и можно использовать везде, где могут использоваться
обычные функции\footnote{В~Common Lisp также можно использовать
\lstinline{LAMBDA}-выражение как аргумент \lstinline{FUNCALL} (или любой другой функции, которая
принимает аргумент-функцию, такой как \lstinline{SORT} или \lstinline{MAPCAR}) без указания
\lstinline!#'! перед нею, например так:
\begin{myverb}
(funcall (lambda (x y) (+ x y)) 2 3)
\end{myverb}
Это разрешено и полностью соответствует версии с использованием \lstinline!#'!. Исторически
\lstinline{LAMBDA}-выражения не были выражениями, которые можно было вычислить. \lstinline{LAMBDA}
не являлось именем функции, макросом или специальным оператором. Вместо этого список,
начинавшийся с символа \lstinline{LAMBDA}, являлся специальной синтаксической конструкцией,
которую Lisp распознавал, как что-то вроде имени функции.
Но если это было бы правдой, то выражение \lstinline{(funcall (lambda (...) ...))} должно быть
неправильным, поскольку \lstinline{FUNCALL} является функцией, и стандартные правила вычислений
для вызова функций должны требовать, чтобы \lstinline{LAMBDA}-выражение было вычислено.
Однако в ходе процесса стандартизации в ANSI, для того чтобы сделать возможным реализацию
ISLISP, другого диалекта Lisp, который стандартизировался в то же самое время, в Common
Lisp был введён макрос \lstinline{LAMBDA}, используемый для совместимости на уровне
пользователей. Этот макрос раскрывается в вызов \lstinline{FUNCTION}, окружающего
\lstinline{LAMBDA}-выражение. Другими словами, следующее \lstinline{LAMBDA}-выражение:
\begin{myverb}
(lambda () 42)
\end{myverb}
\noindent{}раскрывается в следующее, если оно возникает в контексте, где требуется его вычисление:
\begin{myverb}
(function (lambda () 42)) ; или #'(lambda () 42)
\end{myverb}
Это делает возможным использование \lstinline{LAMBDA}-выражений как значений, таких как
аргумент \lstinline{FUNCALL}. Другими словами, это просто синтаксическая обёртка. Большинство
людей либо всегда используют \lstinline!#'! перед \lstinline{LAMBDA}-выражениями, либо никогда
не используют. В~этой книге я всегда буду использовать \lstinline!#'!.}\hspace{\footnotenegspace}.
Анонимные функции могут быть очень полезными, когда вы хотите передать одну функцию в
качестве аргумента другой и она достаточно проста для записи на месте. Например,
предположим, что вы хотите нарисовать график функции \lstinline{2x}. Вы можете определить
следующую функцию:
\begin{myverb}
(defun double (x) (* 2 x))
\end{myverb}
\noindent{}которую затем передать \lstinline{plot}.
\begin{myverb}
CL-USER> (plot #'double 0 10 1)
**
****
******
********
**********
************
**************
****************
******************
********************
NIL
\end{myverb}
\vspace{1.7cm}
Но легче и более понятно написать вот так:
\begin{myverb}
CL-USER> (plot #'(lambda (x) (* 2 x)) 0 10 1)
**
****
******
********
**********
************
**************
****************
******************
********************
NIL
\end{myverb}
Другим очень важным применением \lstinline{LAMBDA}-выражений является их использование для
создания замыканий (closures)~-- функций, которые захватывают часть среды выполнения, в
которой они были созданы. Вы уже использовали замыкания в главе~\ref{ch:03}, но подробное
описание о том, как замыкания работают и как они могут использоваться, больше относится к
переменным, а не к функциям, так что я отложу это обсуждение до следующей главы.
%%% Local Variables:
%%% mode: latex
%%% TeX-master: "pcl-ru"
%%% TeX-open-quote: "<<"
%%% TeX-close-quote: ">>"
%%% End:
Something went wrong with that request. Please try again.