Как начинался компьютер
Компьютерная революция
Двоичный код
Разработки военных лет
Интегральные микросхемы
Микрокомпьютер
Персоны
Сеть
Язык компьютера
Развитие ПО
Гибкие системы
Средства разработки
Информатика
Вычислительная наука
Операционные системы
Искусственный интеллект
Предыстория
Поиск
Знания и рассуждения
Логика
Робототехника
 

 
Язык Лисп Печать
 
Функции

Кроме правил синтаксиса и семантики следующие три компонента составляют основу всех программ на Lisp – функции, переменные и макросы. Вы использовали их во время создания базы данных в главе 3, но я опустил много подробностей о том, как они работают, и как их лучше всего использовать. Я посвящу следующие главы этим вопросам, начав с функций, которые, также как и их аналоги в других языках программирования, обеспечивают основные возможности абстракции.

Большая часть самого Lisp состоит из функций. Более трех четвертей имен, указанных в стандарте, являются именами функций. Все базовые типы данных полностью определены в терминах функций, работающими с ними. Даже мощная объектная система языка Lisp построена на концептуальном развитии понятий функции и обобщенной функции, которые будут описаны в главе 16.

В конце концов, несмотря на важность макросов ( The Lisp Way! ), вся реальная функциональность обеспечивается функциями. Макросы выполняются во время компиляции и создают код программы. После того, как все макросы будут раскрыты, этот код полностью будет состоять из обращения к функциям и специальным операторам. Я не упоминаю, что макросы сами являются функциями, которые используются для генерации кода, а не для выполнения действий в программе. 
 
Определение новых функций

Обычно функции определяются при помощи макроса DEFUN. Типовое использование DEFUN выглядит вот так: 
 
  (defun name (parameter*)
    "Optional documentation string."
    тело-функции*)
 
В качестве имени может использоваться любой символ. Как правило, имена функций содержат только буквы, цифры и знак минус, но, кроме того, разрешено использование других символов, и они используются в определенных случаях. Например, функции, которые преобразуют значения из одного типа в другой, иногда используют символ → в имени. Или функция, которая преобразует строку в виджет, может быть названа string→widget. Наиболее важное соглашение по именованию, заключается в том, что лучше создавать составные имена, используя знак минус вместо подчеркивания или использования заглавных букв внутри имени. Так что frob-widget лучше соответствует стилю Lisp, чем frob_widget или frobWidget.

Список параметров функции определяет переменные, которые будут использоваться для хранения аргументов, переданных при вызове функции. Если функция не принимает аргументов, то список пуст и записывается как (). Различют обязательные, необязательные, множественные, и именованные (keyword) параметры. Эти вопросы будут обсуждаться подробнее в следующем разделе.

За списком параметров может находиться строка, которая описывает назначение функции. После того, как функция определена, эта строка ( строка документации ) будет ассоциирована с именем функции и может быть позже получена с помощью функции DOCUMENTATION.

Тело DEFUN состоит из любого числа выражений Lisp. При вызове функции они вычисляются по порядку, и результат вычисления последнего выражения возвращается, как значение функции. Для возврата из любой точки функции может использоваться специальный оператор RETURN-FROM, что я продемонстрирую через некоторое время.

Мы написали функцию hello-world, которая выглядела вот так: 
 
  (defun hello-world () (format t "hello, world"))
 
Теперь вы можете проанализировать части этой функции. Она называется hello-world, список параметров пуст, потому что она не принимает аргументов, в ней нет строки документации, и ее тело состоит из одного выражения: 
 
  (format t "hello, world")
 
Вот пример немного более сложной функции: 
 
  (defun verbose-sum (x y)
    "Sum any two numbers after printing a message."
    (format t "Summing ~d and ~d.~%" x y)
    (+ x y))
 
Эта функция называется verbose-sum, получает два аргумента, которые связываются с параметрами x и y, имеет строку документации, и ее тело состоит из двух выражений. Значение, возвращенное вызовом функции +, становится значением функции verbose-sum.
 
Списки параметров функций

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

Основное назначение списков параметров – объявление переменных, которые будут использоваться для хранения аргументов, переданных функции. Когда список параметров является простым списком имен переменных, как в verbose-sum, то параметры называются обязательными. Когда функция вызывается, она должна получить ровно по одному аргументу для каждого из обязательных параметров. Каждый параметр связывается с соответствующим аргументом. Если функция вызывается с меньшим или большим количеством аргументов, чем требуется, то Lisp сообщит об ошибке.

Однако, списки параметров в Common Lisp предоставляют более удобные способы отображения аргументов функции в параметры функции. В дополнение к обязательным параметрам функция может иметь необязательные параметры. Или функция может иметь один параметр, который будет связан со списком, содержащим все дополнительные аргументы. И в заключение, аргументы могут быть связаны с параметрами путем использования ключевых слов (keywords), а не путем соответствия позиции параметра и аргумента в списке. Таким образом, списки параметров Common Lisp предоставляют удобное решение для некоторых общих задач кодирования.
 
Необязательные параметры

В то время как многие функции, подобно verbose-sum, нуждаются только в обязательных параметрах, не все функции являются настолько простыми. Иногда функции должны иметь параметр, который будет использоваться только при некоторых вызовах, поскольку он имеет «правильное» значение по умолчанию. Таким примером может быть функция, которая создает структуру данных, которая будет при необходимости расти. Поскольку, структура данных может расти, то не имеет значения, по большей части, какой начальный размер она имеет. Но пользователь функции, который имеет понятие о том, сколько данных будет помещено в данную структуру, может улучшить производительность программы путем указания нужного начального размера этой структуры. Однако, большинство пользователей данной функции, скорее всего, позволят выбрать наиболее подходящий размер автоматически. В Common Lisp вы можете предоставить этим пользователям одинаковые возможности с помощью необязательных параметров; пользователи, которые не хотят устанавливать значение сами, получат разумное значение по умолчанию, а остальные пользователи смогут подставить нужное значение.

Для определения функции с необязательными параметрами после списка обязательных параметров поместите символ &optional, за которым перечислите имена необязательных параметров. Простой пример использования выглядит так: 
 
  (defun foo (a b &optional c d)
    (list a b c d))
 
Когда функция будет вызвана, сначала аргументы связываются с обязательными параметрами. После того, как обязательные параметры получили переданные значения, и остались еще аргументы, то они будут присвоены необязательным параметрам. Если аргументы закончатся до того, как кончится список необязательных параметров, то оставшиеся параметры получат значение NIL. Таким образом, предыдущая функция будет выдавать следующие результаты: 
 
(foo 1 2)     ==> (1 2 NIL NIL)
(foo 1 2 3)   ==> (1 2 3 NIL)
(foo 1 2 3 4) ==> (1 2 3 4)
 
Lisp все равно будет проверять количество аргументов, переданных функции (в нашем случае это число от 2 до 4-х, включительно), и будет выдавать ошибку, если функция вызвана с лишними аргументами, или их, наоборот, не достает.

Конечно, вы можете захотеть использовать другие значения по умолчанию, отличные от NIL. Вы можете указать их, путем замены имени параметра на список, состоящий из имени и выражения. Это выражение будет вычислено только если пользователь не указал значения для необязательного параметра. Общепринятым является простое задание конкретного значения в качестве выражения. 
 
  (defun foo (a &optional (b 10))
    (list a b))
 
Эта функция требует указания одного аргумента, который будет присвоен параметру a. Второй параметр – b, получит либо значение второго аргумента, если он указан, либо число 10. 
 
(foo 1 2) ==> (1 2)
(foo 1)   ==> (1 10)
 
Однако, иногда, вам потребуется большая гибкость в выборе значения по умолчанию. Вы можете захотеть вычислять значение по умолчанию основываясь на других параметрах. И вы можете сделать это – выражение для значения по умолчанию может ссылаться на параметры, ранее перечисленные в списке параметров. Если вы пишете функцию, которая возвращает что-то типа описания прямоугольников, и вы хотите сделать ее удобной для использования с квадратами, то вы можете использовать такой вот список параметров: 
 
  (defun make-rectangle (width &optional (height width))
    ...)
 
что сделает параметр height равным параметру width, если только он не будет явно задан.

Иногда полезно будет знать, было ли значение необязательного параметра задано пользователем, или использовалось значение по умолчанию. Вместо того, чтобы писать код, который проверяет, является ли переданное значение равным значению по умолчанию (это все равно не будет работать, поскольку пользователь может явно задать значение, равное значению по умолчанию), вы можете добавить еще одно имя переменной к списку параметров после выражения для значения по умолчанию. Указанная переменная будет иметь истинное значение, если пользователь задал значение для аргумента, и NIL в противном случае. По соглашению, эти переменные называются также как и параметры, но с добавлением »-supplied-p» к концу имени. Например: 
 
  (defun foo (a b &optional (c 3 c-supplied-p))
    (list a b c c-supplied-p))
 
Выполнение этого кода приведет к следующим результатам: 
 
(foo 1 2)   ==> (1 2 3 NIL)
(foo 1 2 3) ==> (1 2 3 T)
(foo 1 2 4) ==> (1 2 4 T)
 
Остаточные (Rest) параметры

Необязательные параметры применяются только тогда, когда у вас есть отдельные параметры, для которых пользователь может указывать или не указывать значения. Но некоторые функции могут требовать изменяемого количества аргументов. Некоторые встроенные функции, которые вы уже видели, работают именно так. Функция FORMAT имеет два обязательных аргумента – поток вывода и управляющую строку. Но кроме этого, он требует переменное количество аргументов, зависящее от того, сколько значений он должен вставить в управляющую строку. Функция + также получает переменное количество аргументов – нет никаких причин ограничиваться складыванием только двух чисел, эта функция может вычислять сумму любого количества значений. (Она даже может работать вообще без аргументов, возвращая значение 0.) Следующие примеры являются допустимыми вызовами этих двух функций: 
 
  (format t "hello, world")
  (format t "hello, ~a" name)
  (format t "x: ~d y: ~d" x y)
  (+)
  (+ 1)
  (+ 1 2)
  (+ 1 2 3)
 
Очевидно, что вы можете написать функцию с переменным числом аргументов, просто описывая множество необязательных параметров. Но это будет невероятно мучительно – простое написание списка параметров может быть не очень хорошим делом, и это не связывает все параметры с их использованием в теле функции. Для того, чтобы сделать это правильно, вы должны иметь число необязательных параметров равным максимальному допустимому количеству аргументов при вызове функций. Это число зависит от реализации, но гарантируется, что оно будет равно минимум 50. В текущих реализациях оно варьируется от 4,096 до 536,870,911. Этот мозгодробительный подход явно не является хорошим стилем написания программ.

Вместо этого, Lisp позволяет вам указать параметр, который примет все аргументы (этот параметр указывается после символа &rest). Если функция имеет параметр &rest (остаточный параметр), то любые аргументы, оставшиеся после связывания обязательных и необязательных параметров, будут собраны в список, который станет значением остаточного параметра &rest. Таким образом, список параметров для функций FORMAT и + будут выглядеть примерно так: 
 
  (defun format (stream string &rest values) ...)
  (defun + (&rest numbers) ...)
 
Именованые параметры

Необязательные и остаточные (rest) параметры дают вам достаточно гибкости, но ни один из них не помогает вам в следующей ситуации: предположим, что вы имеете функцию, которая получает четыре необязательных параметра. Теперь предположим, что пользователь захочет задать значение только для одного параметра, и даже, что пользователь будет использовать разные параметры.

Пользователи, которые хотят задать значение для первого параметра не имеют никаких проблем – они просто передадут один необязательный параметр, и пропустят оставшиеся. Но что делать пользователям, которые хотят указать значения для других параметров – разве это не та проблема, которую должно решить использование необязательных параметров?

Конечно, это она. Но проблема заключается в том, что необязательные параметры все равно являются позиционными – если пользователь хочет указать четвертый необязательный параметр, то первые три необязательных параметра превращаются для этого пользователя в обязательные. К счастью, существует еще один вид параметров – именованные (keyword) параметры, которые позволяют указывать пользователю, какие значения будут связаны с конкретными параметрами.

Для того, чтобы задать именованные параметры, необходимо после всех требуемых, необязательных и остаточных параметров, указать символ &key и затем перечислить любое количество спецификаторов именованных параметров. Вот пример функции, которая имеет только именованные параметры: 
 
  (defun foo (&key a b c)
    (list a b c))
 
Когда функция вызывается, каждый именованный параметр связывается со значением, которое указано после ключевого слова, имеющего то же имя, что и параметр. Вернемся к главе 4, в которой указывалось, что ключевые слова – это имена, которые начинаются с двоеточия, и которые автоматически определяются как константы, вычисляемые сами в себя (self-evaluating).

Если ключевое слово не указано в списке аргументов, то соответствующий параметр получает значение по умолчанию, точно также как и для необязательный параметр. Поскольку именованные аргументы имеют метку, то они могут быть указаны в любом порядке, если они следуют после обязательных аргументов. Например, foo может быть вызвана вот так: 
 
(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)
 
Также как и для необязательных параметров, именованные параметры могут задавать выражение для вычисления значения по умолчанию и имя supplied-p-переменной. И для необязательных, и для именованных параметров, значение по умолчанию может ссылаться на параметры, указанные ранее в списке. 
 
  (defun foo (&key (a 0) (b 0 b-supplied-p) (c (+ a b)))
    (list a b c b-supplied-p))
 
(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)
 
Также, если по некоторым причинам вы хотите, чтобы пользователь использовал имена аргументов, отличающиеся от имен параметров, то вы можете заменить имя параметра на список, содержащий имя, которое будет использоваться пользователем при вызове, и имя параметра. Следующее определение foo: 
 
  (defun foo (&key ((:apple a)) ((:box b) 0) ((:charlie c) 0 c-supplied-p))
    (list a b c c-supplied-p))
 
позволяет пользователю вызывать функцию вот так: 
 
(foo :apple 10 :box 20 :charlie 30) ==> (10 20 30 T)
 
Этот стиль особенно полезен, если вы хотите полностью отделить публичный интерфейс от деталей внутренней реализации, поскольку обычно внутри вы хотите использовать короткие имена переменных, и значащие имена в программном интерфейсе. Однако, обычно это используется не особо часто.