Множественные значения: values и multiple-value-bind
Знакомимся с необычной возможностью Lisp — возвращать из функции сразу несколько значений, не упаковывая их в структуру.
Множественные значения — механизм, позволяющий функции вернуть несколько значений одновременно через
values, не упаковывая их в список или структуру. Принимают их формойmultiple-value-bind, а лишние значения по умолчанию незаметно отбрасываются.
В большинстве языков функция возвращает ровно одно значение; чтобы вернуть несколько, их заворачивают в массив, кортеж или объект. Common Lisp предлагает иной, элегантный механизм — настоящие множественные значения, которые передаются параллельно и не создают лишних структур. Это тонкая, но очень «лисповая» возможность, и понимание её отличает уверенного программиста от новичка.
Зачем нужны множественные значения
Представим функцию деления с остатком. Логически она даёт два результата — частное и остаток. В языке с одним возвращаемым значением пришлось бы вернуть список (частное остаток) или структуру, а вызывающий код — распаковывать её. Это добавляет «шум»: создаётся объект-контейнер только ради передачи, и его тут же разбирают. Множественные значения решают это напрямую: функция отдаёт оба результата, а вызывающий берёт нужные, без всякого контейнера.
Ключевая идея — что множественные значения не являются списком или иной структурой данных. Это отдельный канал передачи: значения летят «параллельно», как несколько проводов, а не упакованными в одну коробку. Поэтому, когда результат множественных значений используется там, где ждут одно значение, берётся только первое, а остальные тихо отбрасываются — никакой ошибки, никакого «список не там». Это делает механизм необременительным: функция может возвращать дополнительную информацию вторым-третьим значением, и код, которому она не нужна, просто её не замечает.
Этот принцип «не нужно — не мешает» — тонкая, но важная черта дизайна. Он позволяет авторам стандартной библиотеки щедро снабжать функции полезными дополнительными значениями, не усложняя жизнь тем, кому нужен лишь основной результат. Если бы floor возвращал список (частное остаток), каждому пользователю пришлось бы помнить о распаковке даже ради одного частного; а возвращая остаток вторым значением, floor остаётся таким же простым в употреблении, как любая функция с одним результатом, и при этом отдаёт остаток тем, кому он нужен. Это пример того, как продуманный механизм делает «обычный случай» простым, не отнимая возможностей у «продвинутого случая».
values: вернуть несколько значений
Чтобы функция вернула несколько значений, используют values. Многие стандартные функции уже так устроены: floor, truncate, round возвращают и результат округления, и остаток; gethash возвращает значение из таблицы и признак «нашлось ли». Вот как вернуть несколько значений самому:
;; values возвращает несколько значений сразу:
(defun div-mod (a b)
(values (floor a b) (mod a b))) ; частное И остаток
(div-mod 17 5) ; печатает: 3 и 2 (два значения)
;; Стандартный floor тоже даёт два значения:
(floor 17 5) ; => 3 (частное) и 2 (остаток)
;; Но в контексте одного значения берётся только ПЕРВОЕ:
(+ 1 (floor 17 5)) ; => 4 (взято 3, остаток 2 отброшен)
(list (floor 17 5)) ; => (3) (только первое значение!)
Обратите внимание на последние две строки: когда результат floor попадает в обычное выражение, ждущее одно значение, берётся только частное, а остаток молча исчезает. Это и есть «прозрачность» множественных значений: они не мешаются, когда не нужны. Возвращать через values можно сколько угодно значений, в том числе ноль ((values) — функция, не возвращающая ничего).
multiple-value-bind: принять несколько значений
Чтобы принять все возвращённые значения, а не только первое, используют multiple-value-bind. Она связывает несколько локальных переменных с несколькими возвращёнными значениями — по сути, как let, но для множественного результата одной формы. Синтаксис: список имён переменных, форма, дающая множественные значения, и тело.
;; multiple-value-bind принимает несколько значений в переменные:
(multiple-value-bind (quotient remainder)
(floor 17 5)
(format nil "~a с остатком ~a" quotient remainder))
;; => "3 с остатком 2"
;; Если переменных меньше, чем значений — лишние отбрасываются:
(multiple-value-bind (q) (floor 17 5)
q) ; => 3 (остаток просто не принят)
;; Если переменных больше — лишние получают nil:
(multiple-value-bind (a b c) (values 1 2)
(list a b c)) ; => (1 2 NIL) (c не хватило значения)
Это симметрично возврату: values отправляет значения, multiple-value-bind их раскладывает по переменным. Если переменных меньше, лишние значения отбрасываются; если больше — недостающим присваивается nil. Такая «снисходительность» делает механизм гибким: вы берёте ровно столько значений, сколько вам нужно.
Почему это не то же, что вернуть список
Возникает резонный вопрос: зачем отдельный механизм, если можно вернуть список (list частное остаток) и разобрать его? Разница и философская, и практическая. Во-первых, эффективность: множественные значения не создают объект-контейнер в памяти, тогда как список — это новые cons-ячейки, которые потом ещё и собирает сборщик мусора. Во-вторых, прозрачность: результат-список в обычном выражении остаётся списком (его не «развернёшь» автоматически), а множественные значения сами сводятся к первому, когда остальное не нужно. Сравните: (+ 1 (floor 17 5)) работает, а (+ 1 (list 3 2)) — ошибка, потому что нельзя прибавить список.
;; Список НЕ сводится к первому значению автоматически:
;; (+ 1 (list 3 2)) ; ОШИБКА: нельзя прибавить список к числу
(+ 1 (floor 17 5)) ; => 4 а множественные значения — можно
;; Со списком пришлось бы явно распаковывать:
(let ((result (list 3 2)))
(+ 1 (first result))) ; => 4 но это уже ручная распаковка
Смысловое различие тоже важно: множественные значения уместны, когда результаты концептуально отдельны и часто нужен только главный (частное — главное, остаток — дополнение). Список уместен, когда элементы однородны и обрабатываются вместе как коллекция. Возвращать частное и остаток списком — значит навязать вызывающему распаковку даже тогда, когда ему нужно лишь частное. Множественные значения избавляют от этого.
Семейство форм для множественных значений
Вокруг множественных значений есть небольшое семейство удобных форм, и знание их делает работу с механизмом полноценной. multiple-value-list собирает все возвращённые значения в обычный список — мост из мира множественных значений в мир списков, когда они вам всё же нужны вместе. Обратный мост — values-list: он берёт список и возвращает его элементы как множественные значения. nth-value достаёт одно конкретное значение по номеру, не принимая остальные. Эти формы покрывают типовые потребности и избавляют от ручной возни.
;; multiple-value-list собирает значения в список:
(multiple-value-list (floor 17 5)) ; => (3 2) оба значения как список
;; values-list делает обратное — список в множественные значения:
(values-list '(1 2 3)) ; возвращает 1, 2 и 3 как три значения
;; nth-value берёт одно значение по индексу:
(nth-value 1 (floor 17 5)) ; => 2 (только остаток, второе значение)
(nth-value 0 (floor 17 5)) ; => 3 (только частное)
Особенно полезен multiple-value-list: он позволяет «материализовать» множественные значения в список ровно тогда, когда нужно их сохранить, передать или обойти как коллекцию. Связка multiple-value-list и values-list образует пару «упаковать ↔ распаковать» между двумя представлениями. Это показывает, что множественные значения и списки — не враги, а взаимодополняющие механизмы: один оптимален для прозрачной передачи «главное плюс дополнения», другой — для работы с однородной коллекцией, и переход между ними делается одной формой.
Множественные значения и присваивание
Множественные значения интегрированы и с другими конструкциями языка, что делает их по-настоящему удобными. Например, setf в сочетании с values позволяет за одно присваивание разложить несколько значений по нескольким местам — это перекликается с «множественным присваиванием» из современных языков. А макрос multiple-value-setq присваивает возвращённые значения уже существующим переменным (в отличие от multiple-value-bind, который создаёт новые локальные). Эти возможности показывают, что множественные значения — не изолированный трюк, а органичная часть языка, проникающая в разные его уголки.
;; setf с values раскладывает несколько значений по местам:
(let (q r)
(setf (values q r) (floor 17 5))
(list q r)) ; => (3 2)
Как это работает под капотом
Реализация множественных значений хитра и эффективна. Когда функция выполняет values, дополнительные значения передаются по особому, отдельному от основного результата каналу — обычно через специальные регистры или зарезервированную область, а не через создание структуры в куче. Вызывающий код по умолчанию читает лишь основной результат (первое значение), а формы вроде multiple-value-bind явно «подбирают» остальные значения из этого канала. Поэтому когда дополнительные значения никому не нужны, их передача почти ничего не стоит — нет ни создания, ни сборки контейнера.
Стоит отметить, что множественные значения — характерная черта именно Common Lisp; Scheme имеет похожий механизм (values и call-with-values), но пользуется им реже и оформляет иначе. В большинстве же языков аналога нет вовсе, и привычка «вернуть кортеж» туда не дотягивает: кортеж — это всё-таки структура в памяти, а настоящие множественные значения — отдельный канал передачи без упаковки. Это один из примеров, где Lisp предлагает решение тоньше и аккуратнее, чем мейнстрим, и оценить его можно, лишь поработав с ним вживую.
Частые ошибки
Первая ошибка — ждать, что все значения «доедут» сами. Если функция возвращает несколько значений, а вы используете её результат в обычном выражении, доедет только первое. Чтобы получить остальные, нужно явно их принять через multiple-value-bind (или родственные формы). Новички часто удивляются, что остаток от floor «пропал», — он не пропал, его просто не приняли.
Вторая ошибка — путать множественные значения со списком. (values 1 2 3) — это не список (1 2 3); к нему нельзя применить car, его нельзя обойти mapcar. Это отдельный механизм. Если вам действительно нужен список значений, либо возвращайте список явно через list, либо соберите множественные значения в список формой multiple-value-list.
Третья ошибка — использовать множественные значения там, где уместнее структура. Если результатов много, они равноправны и почти всегда нужны вместе, множественные значения становятся неудобными (легко перепутать порядок, тяжело расширять). В таких случаях честнее вернуть именованную структуру или объект. Множественные значения хороши именно для «главное плюс дополнения», а не как замена полноценным составным типам.
Итоги
- Множественные значения позволяют функции вернуть несколько результатов через
valuesбез упаковки в список или структуру — это отдельный канал передачи. - Многие стандартные функции (
floor,round,gethash) возвращают несколько значений; в обычном выражении берётся только первое, остальные отбрасываются. multiple-value-bindпринимает несколько значений в локальные переменные; лишние значения отбрасываются, недостающим присваиваетсяnil.- Это не то же, что вернуть список: множественные значения эффективнее (нет контейнера) и прозрачнее (сами сводятся к первому), тогда как список требует ручной распаковки.
- Множественные значения уместны для «главный результат плюс дополнения»; для множества равноправных результатов лучше структура. В Scheme механизм есть, но используется реже.