Функции: defun и lambda
Учимся создавать функции — именованные через defun и анонимные через lambda — и понимать, как они возвращают результат.
defun определяет именованную функцию, связывая имя с телом и списком параметров. lambda создаёт анонимную функцию — функцию без имени, которую можно передать, вернуть или вызвать на месте. Тело функции — это неявный
progn: выполняются все формы, а возвращается значение последней.
Функции — сердце Lisp; недаром сам язык вырос из идеи записывать рекурсивные функции над символами. В этом разделе мы разберём функции исчерпывающе, и начнём с основ: как функцию определить и как она возвращает результат. Здесь же впервые ясно проступит разница между Common Lisp и Scheme в том, как они обращаются с функциями, — разница, которая будет сопровождать нас весь раздел.
defun: именованная функция
Основной инструмент — defun (define function). Его форма: имя функции, список параметров в скобках и тело — одно или несколько выражений. При вызове параметры связываются с переданными аргументами, тело выполняется, и функция возвращает значение последнего выражения тела.
;; Определение именованной функции:
(defun square (x)
(* x x))
(square 5) ; => 25
(square 12) ; => 144
;; Несколько параметров:
(defun hypotenuse (a b)
(sqrt (+ (* a a) (* b b))))
(hypotenuse 3 4) ; => 5.0
;; Без параметров — пустой список параметров:
(defun greeting ()
"Привет, Lisp!")
(greeting) ; => "Привет, Lisp!"
Обратите внимание: после имени всегда идёт список параметров в скобках, даже если параметров нет (тогда это ()). Это не формальность, а часть единообразия: список параметров — обязательный элемент структуры defun. И заметьте, что мы нигде не пишем «return»: функция возвращает то, что вычислило последнее выражение тела. Это фундаментальное свойство Lisp как выражение-ориентированного языка, к которому мы сейчас вернёмся подробнее.
Тело функции — неявный progn
Тело функции может содержать не одно, а несколько выражений, и они выполняются по порядку. Такой последовательный блок в Lisp называется progn, и тело defun ведёт себя как неявный progn: все выражения вычисляются одно за другим (ради побочных эффектов — печати, присваиваний), а наружу возвращается значение последнего. Промежуточные значения отбрасываются.
;; Тело из нескольких выражений — неявный progn:
(defun describe-number (n)
(print n) ; побочный эффект (печать), значение отброшено
(print (* n n)) ; ещё печать, значение отброшено
(if (evenp n) 'even 'odd)) ; ВОЗВРАЩАЕТСЯ значение последнего выражения
(describe-number 4)
;; печатает 4 и 16, затем возвращает EVEN
Это объясняет, почему в Lisp обычно нет нужды в явном операторе возврата: значение функции — естественный результат вычисления её тела. Конечно, для досрочного выхода существует return-from, но в большинстве функций он не нужен — структура строится так, чтобы нужное значение оказалось последним выражением. Такой стиль приучает мыслить функцию как выражение, дающее результат, а не как последовательность команд, что заметно отличается от императивных языков.
Эта особенность — часть более широкого свойства Lisp: он выражение-ориентированный язык, в отличие от оператор-ориентированных C, Java и им подобных. В оператор-ориентированном языке есть принципиальная разница между «оператором» (делает что-то, но не имеет значения) и «выражением» (вычисляется в значение). В Lisp этого деления почти нет: почти всё является выражением и возвращает значение. Даже if — не оператор, а выражение, дающее значение одной из ветвей, поэтому его можно использовать прямо там, где нужно значение. Из-за этого код на Lisp естественно складывается из вложенных выражений, каждое из которых что-то возвращает, и понятие «инструкции, не дающей значения» почти не встречается. Привыкнув к этому, начинаешь воспринимать программу не как список команд для машины, а как одно большое вычисляемое выражение, — и это, возможно, главный сдвиг мышления при переходе на Lisp.
lambda: анонимная функция
Не каждой функции нужно имя. Часто функция нужна одноразово — передать её в mapcar, использовать как обработчик, вернуть из другой функции. Для этого служит lambda — она создаёт функцию «на месте», без имени. Синтаксис тот же, что у defun, но без имени: (lambda (параметры) тело). Само слово lambda пришло из лямбда-исчисления Чёрча, на которое опирался Маккарти, создавая Lisp.
;; Анонимная функция, вызванная сразу на месте:
((lambda (x) (* x x)) 5) ; => 25
;; Чаще lambda передают другой функции:
(mapcar (lambda (x) (* x 10))
'(1 2 3)) ; => (10 20 30)
;; lambda с несколькими параметрами:
(mapcar (lambda (a b) (+ a b))
'(1 2 3) '(10 20 30)) ; => (11 22 33)
Анонимные функции — основа функционального стиля. Когда логика проста и используется в одном месте, заводить ей имя через defun избыточно; lambda позволяет выразить её прямо там, где она нужна. По сути defun можно представить как «создать lambda и привязать к имени» — анонимная функция первична, а именование вторично.
Строки документации и соглашения об именах
У defun есть приятная встроенная возможность — строка документации (docstring). Если первое выражение тела — строковый литерал (а тело содержит и другие выражения), эта строка не отбрасывается, а сохраняется как документация функции, доступная во время работы программы через documentation. Это часть «живой» природы Lisp: описание функции хранится в самом образе и доступно интерактивно, а не только в комментарии исходника.
;; Первый строковый литерал в теле — это docstring:
(defun celsius-to-fahrenheit (c)
"Переводит температуру из Цельсия в Фаренгейт."
(+ (* c 9/5) 32))
(celsius-to-fahrenheit 100) ; => 212
(documentation 'celsius-to-fahrenheit 'function)
;; => "Переводит температуру из Цельсия в Фаренгейт."
Стоит сказать и о принятых в Lisp соглашениях именования функций — они выразительны и помогают читать код. Имена-вопросы, оканчивающиеся на p (от predicate), — это предикаты, возвращающие истину или ложь: evenp (чётное?), null (пусто?), listp (список?). Имена, начинающиеся с n, как мы видели в разделе про списки, часто означают деструктивные версии. Имена с восклицательным знаком в Scheme (set!) означают изменение. Префиксы make- обозначают конструкторы. Эти соглашения не навязаны компилятором, но соблюдаются повсеместно, и знание их превращает чтение незнакомого кода в куда более лёгкую задачу: уже по имени функции можно угадать её природу.
Контраст со Scheme: define и единый вызов
Здесь Common Lisp и Scheme заметно расходятся, и это первое проявление различия Lisp-2 против Lisp-1. В Scheme функции определяют через define, а анонимные — тоже через lambda, но дальше начинается разница. В Scheme функция — это обычное значение, живущее в том же пространстве имён, что и переменные. Поэтому анонимную функцию можно вызвать и передать без всякого специального синтаксиса, и определение функции — это просто связывание имени с лямбдой.
; Scheme (R7RS): define для именованных функций
(define (square x) (* x x))
(square 5) ; => 25
; Это в точности то же, что связать имя с lambda:
(define square (lambda (x) (* x x)))
(square 5) ; => 25
; Анонимная функция вызывается напрямую:
((lambda (x) (* x x)) 5) ; => 25
Внешне всё похоже, но за этим стоит глубокое различие. В Scheme (define square ...) и (define x 5) кладут имена в одно пространство — функция square и переменная x равноправны, обе суть «значения». В Common Lisp функция square живёт в отдельном функциональном пространстве имён, и поэтому, чтобы взять её как значение или вызвать функцию, хранящуюся в переменной, нужен особый синтаксис — #' и funcall. Этой ключевой теме посвящён последний урок раздела; пока достаточно заметить, что в Scheme функции «прозрачнее» — они просто значения, а в Common Lisp у них особый статус.
Как это работает под капотом
Когда вы выполняете defun, Common Lisp компилирует тело функции в машинный код (в SBCL — сразу) и связывает имя функции с этим кодом в функциональной ячейке символа. Напомним из раздела про символы: символ в Common Lisp имеет раздельные ячейки под значение-переменную и под функцию. defun заполняет именно функциональную ячейку, поэтому имя square как функция и потенциальная переменная square не конфликтуют. lambda же создаёт объект-функцию, не связывая его ни с каким именем, — он просто существует как значение, которое можно передать.
Важно понимать, что и defun, и lambda порождают полноценные функциональные объекты первого класса: их можно хранить, передавать, возвращать. Именно это делает Lisp функциональным языком в полном смысле. Параметры функции при вызове связываются лексически — то есть видны только внутри тела этой функции, — что соответствует обычной интуиции об аргументах. Тонкости лексического и динамического связывания мы детально разберём в следующем разделе; здесь же главное — что вызов создаёт новые локальные привязки параметров, не затрагивая ничего снаружи.
Частые ошибки
Первая ошибка новичка — искать оператор return. В Lisp его обычно не нужно: функция возвращает значение последнего выражения тела. Попытка дописать что-то «после возврата» приведёт к тому, что именно это «что-то» и станет возвращаемым значением, а предполагаемый результат потеряется. Нужно строить тело так, чтобы итоговое значение было последним выражением.
Вторая ошибка — забыть скобки вокруг списка параметров или перепутать его с телом. (defun f x ...) — ошибка; правильно (defun f (x) ...), даже для одного параметра. Список параметров всегда заключён в свои скобки, отделяющие его от тела.
Третья ошибка, особенно при переходе из Scheme, — пытаться вызвать функцию из переменной напрямую в Common Lisp. Если вы положили функцию в переменную f и пишете (f 5), Common Lisp будет искать функцию с именем f, а не значение переменной f — из-за раздельных пространств имён. Нужен (funcall f 5). В Scheme же (f 5) сработает, потому что пространство имён единое. Это различие — источник постоянной путаницы между диалектами, и мы посвятим ему отдельный урок.
Итоги
defunопределяет именованную функцию (имя, список параметров в скобках, тело);lambdaсоздаёт анонимную функцию без имени.- Тело функции — неявный
progn: все выражения выполняются по порядку, возвращается значение последнего; явный return обычно не нужен. - Анонимные функции (
lambda) — основа функционального стиля;defunпо сути привязываетlambdaк имени. - В Scheme функции определяют через
defineи они суть обычные значения в едином пространстве имён; в Common Lisp функции живут в отдельной функциональной ячейке (Lisp-2). - И
defun, иlambdaсоздают функциональные объекты первого класса; в Common Lisp функцию из переменной вызывают черезfuncall, а не напрямую.