Регистрация | Войти
Lisp — программируемый язык программирования
Предыдущая Оглавление Следующая

5. Функции

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

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

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

Определение новых функций

Обычно функции определяются при помощи макроса DEFUN. Типовое использование DEFUN выглядит вот так:

(defun name (parameter*)
  "Optional documentation string."
  тело-функции*
)

В качестве имени может использоваться любой символ.2) Как правило, имена функций содержат только буквы, цифры и знак минус, но, кроме того, разрешено использование других символов, и они используются в определенных случаях. Например, функции, которые преобразуют значения из одного типа в другой, иногда используют символ -> в имени. Или функция, которая преобразует строку в виджет, может быть названа string->widget. Наиболее важное соглашение по именованию, затронутое в главе 2, заключается в том, что лучше создавать составные имена, используя знак минус вместо подчеркивания или использования заглавных букв внутри имени. Так что frob-widget лучше соответствует стилю Lisp, чем frob_widget или frobWidget.

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

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

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

В главе 2 мы написали функцию 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 вы можете предоставить этим пользователям одинаковые возможности с помощью необязательных параметров; пользователи, которые не хотят устанавливать значение сами, получат разумное значение по умолчанию, а остальные пользователи смогут подставить нужное значение.5)

Для определения функции с необязательными параметрами после списка обязательных параметров поместите символ &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.6) Хех! Этот мозгодробительный подход явно не является хорошим стилем написания программ.

Вместо этого, 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, в которой указывалось, что ключевые слова – это имена, которые начинаются с двоеточия, и которые автоматически определяются как константы, вычисляемые сами в себя FIXME (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)

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

Совместное использование разных типов параметров

Использование всех четырех типов параметров в одной функции хотя и является вполне возможным, но применяется редко. Когда используется более одного типа параметров, они должны быть объявлены в порядке, который мы уже обсуждали – сначала указываются имена требуемых параметров, затем - необязательных, потом - остаточных (&rest), и в заключение - именованных параметров. Но обычно в функциях, которые используют несколько типов параметров, комбинируют требуемые параметры с одним из других видов параметров, или возможно комбинируют необязательные и остаточные параметры. Два других сочетания – необязательных или остаточных параметров с именованными параметрами, могут привести к очень удивительному поведению функции.

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

(defun foo (x &optional y &key z) 
  (list x y z)
)

Если она вызывается вот так, то все нормально:

(foo 1 2 :z 3) ==> (1 2 3)

И вот так, все работает нормально:

(foo 1)  ==> (1 nil nil)

Но в этом случае, она выдает ошибку:

(foo 1 :z 3) ==> ERROR

Это происходит потому, что имя параметра :z берется как значение для необязательного параметра y, оставляя для обработки только аргумент 3. При этом, Lisp ожидает, что в этом месте встретится либо пара имя/значение, либо не будет ничего, и одиночное значение приведет к выдаче ошибки. Будет даже хуже, если функция будет иметь два необязательных параметра, так что использование функции как в последнем примере, приведет к тому, что значения :z и 3 будут присвоены двум необязательным параметрам, а именованный параметр z получит значение по умолчанию – NIL, без всякого указания, что что-то произошло неправильно.

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

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

(defun foo (&rest rest &key a b c) 
  (list rest a b c)
)

вы получите следующие результаты:

(foo :a 1 :b 2 :c 3)  ==> ((:A 1 :B 2 :C 3) 1 2 3)

Возврат значений из функции

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

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

Вы увидите в главе 20 что RETURN-FROM в самом деле не привязана к функциям; она используется для возврата из блока кода, определенного с помощью оператора BLOCK. Однако, DEFUN автоматически помещает тело функции в блок кода с тем же именем, что и имя функции. Так что, вычисление RETURN-FROM с именем функции и значением, которое вы хотите возвратить, приведет к немедленному выходу из функции с возвратом указанного значения. RETURN-FROM является специальным оператором, чьим первым аргументом является имя блока из которого необходимо выполнить возврат. Это имя не вычисляется, так что нет нужды его экранировать.

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

(defun foo (n)
  (dotimes (i 10)
    (dotimes (j 10)
      (when (> (* i j) n)
        (return-from foo (list i j))
)
)
)
)

Надо отметить, что необходимость указания имени функции из которой вы хотите вернуться, является не особо удобной – если вы измените имя функции, то вам нужно будет также изменить имя, использованное в операторе RETURN-FROM.8) Но следует отметить, что явное использование RETURN-FROM в Lisp происходит значительно реже, чем использование выражения return в C-подобных языках, поскольку все выражения Lisp, включая управляющие конструкции, такие как условные выражения и циклы, вычисляются в значения. Так что это не представляет особой сложности на практике.

Функции как данные, или Функции высшего порядка

В то время как основной способ использования функций – это вызов их с указанием имени, существуют ситуации, когда было бы полезно рассматривать функции как данные. Например, вы можете передать одну функцию в качестве аргумента другой функции, вы можете написать общую функцию сортировки, и предоставить пользователю возможность указания функции для сравнения двух элементов. Так что один и тот же алгоритм может использоваться с разными функциями сравнения. Аналогично, обратные вызовы (callbacks) и FIXME hooks зависят от возможности хранения ссылок на исполняемый код, который можно выполнить позже. Поскольку функции уже являются стандартным способом представления частей кода, имеет смысл разрешить рассмотрение функций как данных.9)

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

Специальный оператор FUNCTION обеспечивает механизм получения объекта-функции. Он принимает единственный аргумент и возвращает функцию с этим именем. Имя не экранируется. Так что, если вы определили функцию foo, например вот так:

CL-USER> (defun foo (x) (* 2 x))
FOO

вы можете получить объект-функцию следующим образом:10)

CL-USER> (function foo)
#<Interpreted Function FOO>

В действительности, вы уже использовали FUNCTION, но это было замаскировано. Синтаксис #', который вы использовали в главе 3, является синтаксической оберткой для FUNCTION, точно также как и ' является оберткой для QUOTE.11) Так что вы можете получить объект-функцию вот так:

CL-USER> #'foo
#<Interpreted Function FOO>

После того, как вы получили объект-функцию, есть только одна вещь, которую вы можете сделать с ней – выполнить ее. Common Lisp предоставляет две функции для выполнения функции через объект-функцию: FUNCALL и APPLY.12) Они отличаются тем, как они получают аргументы, которые будут переданы вызываемой функции.

FUNCALL это функция, которая используется тогда, когда во время написания кода вы знаете количество аргументов, которые вы будете передавать функции. Первым аргументом FUNCALL является запускаемый объект-функция, а оставшиеся аргументы передаются данной функции. Так что следующие два выражения являются эквивалентными:

(foo 1 2 3) === (funcall #'foo 1 2 3)

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

Следующая функция демонстрирует более реалистичное использование FUNCALL. Она принимает объект-функцию в качестве аргумента, и рисует простую текстовую диаграмму значений возвращенных функцией, вызываемой для значений от min до max с шагом step.

(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 "~%")
)
)

Выражение FUNCALL вычисляет значение функции для каждого значения i. Внутрениий цикл использует это значение для определения того, сколько раз напечатать знак "звездочка".

Заметьте, что вы не используете FUNCTION или #' для получения значения fn; вы хотите, чтобы оно интерпретировалось как переменная, поскольку значение этой переменной является объектом-функцией. Вы можете вызвать plot с любой функцией, которая берет один числовой аргумент, например, со встроенной функцией EXP, которая возвращает значение e, возведенное в степень переданного аргумента.

CL-USER> (plot #'exp 0 4 1/2)
*
*
**
****
*******
************
********************
*********************************
******************************************************
NIL

Однако FUNCALL не особо полезен, когда список аргументов становится известен только во время выполнения. Например, для работы с функцией plot в других случаях, представьте, что вы получили список, содержащий объект-функцию, минимальное и максимальное значения, а также шаг изменения значений. Другими словами, список содержит значения, которые вы хотите передать как аргументы для plot. Предположим, что этот список находится в переменной plot-data. Вы можете вызвать plot с этими значениями вот так вот:

(plot 
  (first plot-data)
  (second plot-data)
  (third plot-data)
  (fourth plot-data)
)

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

Это как раз тот случай, когда на помощь приходит APPLY. Подобно FUNCALL, ее первым аргументом является объект-функция. Но после первого аргумента, вместо перечисления отдельных аргументов, она принимает список. Затем APPLY применяет функцию к значениям в списке. Это позволяет вам переписать предыдущий код следующим образом:

(apply #'plot plot-data)

Кроме того, APPLY может также принимать "свободные" аргументы, также как и обычные аргументы в списке. Таким образом, если plot-data содержит только значения для min, max и step, то вы все равно можете использовать APPLY для отображения функции EXP используя следующее выражение:

(apply #'plot #'exp plot-data)

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

Анонимные функции

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

Когда кажется, что определение новых функций с помощью DEFUN является излишним, вы можете создать "анонимную" функцию, используя выражение LAMBDA. Как обсуждалось в главе 3, LAMBDA-выражение выглядит примерно так:

(lambda (parameters) body)

Можно представить себе, что LAMBDA-выражения – это специальный вид имен функций, где само имя напрямую описывает что эта функция делает. Это объясняет, почему вы можете использовать LAMBDA-выражение вместо имени функции с #'.

(funcall #'(lambda (x y) (+ x y)) 2 3) ==> 5

Вы даже можете использовать LAMBDA-выражение как "имя" функции в выражениях, вызывающих функцию. Если вы хотите, то вы можете переписать предыдущий пример с FUNCALL в следующем виде.

((lambda (x y) (+ x y)) 2 3) ==> 5

Но обычно так никогда не пишут, это использовалось лишь для демонстрации, что LAMBDA-выражения разрешено и можно использовать везде, где могут использоваться обычные функции.13)

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

(defun double (x) (* 2 x))

которую затем передать plot.

CL-USER> (plot #'double 0 10 1)
**
****
******
********
**********
************
**************
****************
******************
********************
NIL

Но легче и более понятно написать вот так:

CL-USER> (plot #'(lambda (x) (* 2 x)) 0 10 1)
**
****
******
********
**********
************
**************
****************
******************
********************
NIL

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

1)Несмотря на важность функций в Common Lisp, не совсем правильно называть его функциональным языком. Это правильно для некоторой части возможностей Common Lisp, таких как функции работы со списками, которые созданы для работы в стиле тело-form*!!!. Конечно же Lisp занимает значительное место в истории функционального программирования – McCarthy ввел в обращение много идей, которые считаются очень важными для функционального программирования, но Common Lisp был умышленно спроектирован для поддержки разных стилей программирования. В семействе Lisp-подобных языков, язык Scheme является наиболее близким к понятию "чистого" функционального языка, но даже он имеет несколько возможностей, которые отделяют его от чистоты таких языков как Haskell и ML.
2)Хорошо, почти любой символ. Неопределенным является поведение, когда вы в качестве имени для ваших функций используете одно из имен, указанных в стандарте. Однако, как вы увидите в главе 21, система пакетов Lisp позволяет вам создавать имена в разных пространствах имен, так что это не является проблемой.
3)Списки параметров иногда называются лямбда-списками из-за исторического отношения между понятием функции в Lisp и лямбда-исчислением.
4)Например, следующий код:
(documentation 'foo 'function)
вернёт строку документации для функции foo. Однако, заметьте, что документация предназначается для людей, а не для работы программ. Реализации Lisp не обязаны сохранять их и могут удалить их в любое время, так что переносимые программы не должны зависеть от наличия документации к функции. В некоторых реализациях требуется установка специальных переменных, имена которых зависят от конкретной реализации, чтобы они начали хранить документацию к функциям.
5)В языках, которые явно не поддерживают необязательные параметры, программисты обычно находят методы их эмуляции. Одна из техник заключается в использовании предопределенных значений "не-значение", которые пользователь может передать, показывая, что он хочет использовать значение по умолчанию. В языке C, например, часто используют NULL в качестве такого предопределенного значения. Однако, такая договоренность между функцией и ее пользователями является лишь подпоркой – в некоторых функциях или для некоторых аргументов предопределенным значением может быть NULL, в то время как для других функций или для других аргументов таким значением может быть -1 или некоторая другая предопределенная (часто заданная с помощью #define) константа.
6)Для вашей реализации вы можете узнать это значение используя константу CALL-ARGUMENTS-LIMIT.
7)четыре стандартные функции принимают необязательные и именованные аргументы – READ-FROM-STRING, PARSE-NAMESTRING, WRITE-LINE и WRITE-STRING. Их оставили во время стандартизации для обеспечения обратной совместимости с более ранними диалектами Lisp. READ-FROM-STRING является лидером по количеству ошибок сделанных начинающими программистами на Lisp – вызов этой функции как (read-from-string s :start 10) игнорирует ключевое слово :start, читает с индекса 0, а не с 10. Это происходит, поскольку READ-FROM-STRING имеет два необязательных параметра, которые съедают аргументы :start и 10.
8)Другой макрос, RETURN, не требует указания имени блока. Однако, вы не можете использовать его вместо RETURN-FROM для того, чтобы не указывать имя функции – это лишь синтаксическая обвязка для возврата из блока с именем NIL. Описание этого макроса, вместе с описанием BLOCK и RETURN-FROM, будет сделано в главе 20.
9)Конечно, Lisp не является единственным языком, который позволяет рассматривать функции как данные. Язык C использует указатели на функции, Perl использует ссылки на подпрограммы, Python использует подход, аналогичный Lisp, а C# ввел делегаты (типизированные указатели на функции, призванные улучшить механизмы используемые в Java) и также механизм анонимных классов.
10)Точное печатное представление объекта-функции может отличаться в зависимости от реализации.
11)Лучше всего рассматривать FUNCTION как специальный вид экранирования. Экранирование символа предотвращает его вычисление, оставляя сам символ, а не значение переменной с именем символа. FUNCTION также изменяет нормальные правила вычисления, но вместо предотвращения вычисления символа, заставляет вычислять его как имя функции, точно также, как если бы этот символ использовался в качестве имени функции в выражении вызова.
12)В действительности существует и третья возможность, специальный оператор MULTIPLE-VALUE-CALL, но я отложу этот вопрос до того момента, когда мы будет обсуждать выражения, возвращающие множественные значения в главе 20.
13)В Common Lisp также возможно использовать LAMBDA-выражение как аргумент FUNCALL (или любой другой функции, которая принимает аргумент-функцию, такой как SORT или MAPCAR) без указания #' перед нею, например так:
(funcall (lambda (x y) (+ x y)) 2 3)
Это разрешено и полностью соответствует версии с использованием #'. Исторически, LAMBDA-выражения не были выражениями, которые можно было вычислить. LAMBDA не являлось именем функции, макросом или специальным оператором. Вместо этого, список, начинавшийся с символа LAMBDA, являлся специальной синтаксической конструкцией, которую Lisp распознавал, как что-то вроде имени функции. Но если это было бы правдой, то выражение (funcall (lambda (...) ...)) должно быть неправильным, поскольку FUNCALL является функцией, и стандартные правила вычислений для вызова функций должны требовать, чтобы LAMBDA-выражение было вычислено. Однако, в ходе процесса стандартизации в ANSI для того, чтобы сделать возможным реализацию ISLISP, другого диалекта Lisp, который стандартизировался в то же самое время, в Common Lisp был введен макрос LAMBDA, используемый для совместимости на уровне пользователей. Этот макрос раскрывается в вызов FUNCTION, окружающего LAMBDA-выражение. Другими словами, следующее LAMBDA-выражение:
(lambda () 42)
раскрывается в следующее, если оно возникает в контексте, где требуется его вычисление:
(function (lambda () 42))   ; или #'(lambda () 42)
Это делает возможным использование LAMBDA-выражений как значений, таких, как аргумент FUNCALL. Другими словами, это просто синтаксическая обертка. Большинство людей либо всегда используют #' перед LAMBDA-выражениями, либо никогда не используют. В этой книге я всегда буду использовать #'.
Предыдущая Оглавление Следующая
@2009-2013 lisper.ru