Язык Лисп
 
Понятие списка интересовало и другого выдающегося члена тесного сообщества исследователей искусственного интеллекта - Джона Маккарти, блестящего и разностороннего математика.

В 1958 г., когда при его помощи организовалась лаборатория по искусственному интеллекту при МТИ, Маккарти начал работу над языком высокого уровня, в котором понятие списка сочеталось с системой обозначений, заимствованной из экзотической области математики так называемого лямбда-исчисление.

Введенное в 1931 г. математиком и логиком Алонзо Чёрч, это исчисление оперирует только тремя элементами: символами, представляющими переменные и константы, скобками для группировки символов и обозначениями функций, выражаемыми греческой буквой «лямбда».

Маккарти назвал свой язык Лисп (LISP, от LISt Processing Language - обработка списков).

Даже небольшой фрагмент про граммы на Лиспе может содержать десятки пар скобок, определяющих списки. Часто пары скобок вкладываются друг в друга, образуя список внутри списка, который в свою очередь также находится внутри списка (иногда бывает 8-10 уровней вложенности). В списке (PUT(QUOTE SHIP)(QUOTE LOC)(QUOTE (7 5») функция PUT назначает для размещения (LOCation) корабля (SHIP) декартовы координаты (7,5); функция QUOTE показывает, что нужно использовать имя списка или просто символ, а не его значение; символ LOC - имя свойства (в нашем случае размещения), принадлежащего символу SHIP.

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

Специалисты по информатике восхищались как элегантностью, так и утилитарностью Лиспа, особенно после публикации в 1960 г. широко известной статьи Маккарти под заглавием «Рекурсивные функции и символьные вычисления», в которой излагалось математическое обоснование применения Лиспа.

«Лисп был для нас не просто языком, который используют для определенных целей, говорил Пол Абрахамс, бывший дипломником у Маккарти в период разработки нового языка, - им можно было любоваться, как прелестной вещью. Поэтому существовала постоянная напряженность в отношениях между теми, кто восхищался Лиспом за его чистоту, и теми, кто стремился использовать его для различных вычислений. Безусловно, с помощью Лиспа проделано множество вычислений. Но в самом начале было не так. Часто говорили, что главная цель Лиспа - делать больше Лиспа».

Сегодня в США Лисп остается основным языком программирования в исследованиях по искусственному интеллекту. Скорость и легкость, с которой можно писать, выполнять и видоизменять программы на Лиспе, способствовали появлению многочисленных поклонников этого языка и за пределами лабораторий искусственного интеллекта.

Подобно большинству популярных языков, Лисп породил обширное потомство. Например, в Великобритании признанным языком для искусственного интеллекта является производный от Лиспа язык ПОП-2 (POP-2), созданный в 60-х годах и названный в честь одного из его соавторов, Робина Дж. Попплстона (Robin Popplestone) из Эдинбургского университета (University of Edinburgh). 

 
Если вы считаете, что наибольшее удовольствие в программировании доставляют большие результаты, достигнутые с помощью кода, просто и прозрачно выражающего ваши желания, тогда программирование на Common Lisp будет самым приятным из того, что вы можете делать на компьютере. Используя Common Lisp, вы достигнете б́ольших результатов в б́олее короткий срок, чем используя другие языки программирования.

Серьёзное заявление. Могу ли я доказать это? Да, но не на нескольких страницах введения. Вам придётся познакомиться с Lisp поближе и убедиться в этом самим — так что всё-таки придётся читать книгу до конца. А сейчас, позвольте мне начать с нескольких смешных эпизодов, с истории моего пути к языку Lisp. В следующей главе я объясню выгоды, которые вы получите от изучения Common Lisp.

Я один из немногих Lisp хакеров второго поколения. Мой отец начал заниматься компьютерами с написания на ассемблере операционной системы для машины, которую он использовал для сбора данных при подготовке его докторской диссертации по физике. После работы с компьютерами в разных лабораториях физики, к 80-м, отец полностью оставил физику и стал работать в большой фармацевтической компании. У этой компании был проект создания программы, моделирующей производственные процессы на химических заводах (если вы увеличите размер данного сосуда, как это повлияет на годовые производственные показатели?). Старая команда писала всё на языке FORTRAN, использовала половину бюджета и почти всё отведённое время, и, тем не менее, им нечем было гордиться. Это было в 80-х, во время бума искусственного интеллекта (ИИ), Lisp так и витал в воздухе. Так что мой папа — в то время еще не поклонник языка Lisp — пошёл в университет Карнеги-Меллона, чтобы пообщаться с людьми, работавшими над тем, что впоследствии стало Common Lisp, и узнать, поможет ли Lisp его проекту.

Ребята из университета показали ему несколько демонстраций того над чем они в тот момент работали, и он был убежден. Отец, в свою очередь, убедил своих боссов позволить его команде взять провальный проект и сделать его на Lisp. Год спустя, используя только то, что осталось от исходного бюджета, команда отца представила работающее приложение, обладающее возможностями, на реализацию которых старая команда уже не надеялась. Мой папа объясняет, что причина успеха в решении использовать Lisp.

Однако это всего лишь первый эпизод. Может быть, мой отец ошибался в причине своего успеха. Или, может быть, Lisp был лучше других языков того времени. В настоящее время мы имеем кучу новых языков программирования, многие из которых переняли часть достоинств Lisp. Действительно ли я считаю, что использование языка Lisp может дать вам те же выгоды, что и моему отцу в 80-х? Читайте дальше.

Несмотря на все усилия моего отца, я не изучал Lisp в университете. После учёбы, которая не содержала много программирования на каком-либо языке, я был покорен Web и вернулся назад к компьютерам. Сначала я писал на Perl, изучив его достаточно, чтобы создать форум для сайта журнала Mother Jones, после этого я работал над большими (по тем временам) сайтами, такими, как, например, сайт компании Nike, запущенный к олимпийским играм 1996 года. После этого я перешёл на Java, будучи одним из первых разработчиков в WebLogic (теперь эта компания — часть BEA). После WebLogic я участвовал в другом стартапе, где был ведущим программистом по построению транзакционной системы обмена сообщениями на Java. Со временем, мои основные интересы в программировании позволили мне использовать как популярные языки, такие как C, C++ и Python, так и менее известные, такие как Smalltalk, Eiffel и Beta.

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

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

Похожий эксперимент привёл к созданию библиотеки, о которой я расскажу в главе 24. В начале моей карьеры в WebLogic я написал библиотеку на Java для разбора java-классов (файлов *.class). Она работала, но код был запутан и его трудно было изменить или добавить новую функциональность. В течение нескольких лет я пытался переписать библиотеку, думая, что смогу использовать мои новые знания в Java и не увязнуть в куче дублирующегося кода, но так и не смог. Когда же я попробовал написать её на Common Lisp, это заняло всего 2 дня, и я получил не просто библиотеку для разбора java-классов, но библиотеку для разбора любых двоичных файлов. Вы увидите, как она работает, в главе 24, и используете её в главе 25 для разбора тэгов ID3 в MP3-файлах.
 
Почему Lisp?

Сложно объяснить на нескольких страницах введения, почему пользователи языка любят именно какой-то конкретный язык, ещё сложнее объяснить, почему вы должны тратить своё время на его изучение. Личный пример не слишком убеждает. Может быть, я люблю Lisp, потому что какая-то цепь в моём мозгу замкнулась. Это может быть даже генетическим отклонением, так как мой отец похоже тоже имел его. Так что прежде, чем вы погрузитесь в изучение языка Lisp, вполне естественным кажется желание узнать, что это вам даст, какую выгоду принесёт.

Для некоторых языков выгода очевидна. Например, если вы хотите писать низкоуровневые программы для Unix, то должны выучить C. Или если вы хотите писать кросс-платформенные приложения, то должны использовать Java. И большое число компаний до сих пор использует C++, так что если вы хотите получить работу в одной из них, то должны знать C++.

Тем не менее, для большинства языков выгоду не так просто выделить. Мы имеем дело с субъективными оценками того, насколько язык удобно использовать. Защитники Perl любят говорить, что он «делает простое вещи простыми, а сложные - возможными» и радуются факту, озвученному в девизе Perl - «Есть более, чем один способ сделать это».1) С другой стороны, фанаты языка Python думают, что Python прозрачный и простой язык, и код на Python проще понять, потому что, как гласит их лозунг, «Есть лишь один способ сделать это».

Так почему же Common Lisp? Здесь нет такой очевидной выгоды, как для C, Java или C++ (конечно, если вы не является счастливым обладателем Lisp-машины). Выгоды от использования Lisp заключены в переживаниях и впечатлениях от его использования. В остальной части книги я буду показывать отличительные черты языка, так что вы сможете по себе оценить, на что эти впечатления похожи. Сейчас я попытаюсь показать смысл философии Lisp.

В качестве девиза для Common Lisp лучше всего подходит похожее на дзенский коан описание «программируемый язык программирования». Хотя и несколько запутанный, данный девиз, тем не менее, выделяет суть преимущества, которое Lisp до сих пор имеет перед другими языками программирования. Больше, чем другие языки, Common Lisp следует философии: что хорошо для разработчика языка, то хорошо для его пользователей. Программируя на Common Lisp, вы, скорее всего, никогда не обнаружите нехватки каких-то возможностей в языке, которые упростили бы программирование, потому что, как будет показано далее, вы можете просто добавить эти возможности в язык.
 

Следовательно, программы на Common Lisp стараются предоставить наиболее прозрачное отображение между вашими идеями о том, как программа должна работать, и кодом, который вы пишете. Ваши идеи не замутняются нагромождением кода и бесконечно повторяющимися выражениями. Это делает ваш код более управляемым, потому что вам больше не приходится бродить по нему всякий раз, когда вы хотите внести какие-то изменения. Даже систематические изменения в программе могут быть достигнуты относительно малыми изменениями исходного кода. Это также означает, что вы будете писать код быстрее; вы будете писать меньше кода и не будете терять время на поиск пути для выражения своих идей в ограничениях, накладываемых языком программирования).

Common Lisp — это также прекрасный язык для исследовательского программирования (прототипирования?), когда вам неизвестно достоверно, как ваша программа должна работать. Common Lisp предоставляет некоторые возможности, помогающие вам вести инкрементальную интерактивную разработку.

Интерактивный цикл read-eval-print, о котором я расскажу в следующей главе, позволяет вам непрерывно взаимодействовать с вашей программой во время её разработки. Пишите новую функцию. Тестируете её. Меняете её. Пробуете другие подходы к реализации. Вам не приходится останавливаться для длительной компиляции3).

Другие поддерживаемые возможности: быстрый, интерактивный стиль программирования, динамическая типизация Lisp и система обработки ситуаций. Первое позволяет вам тратить меньше времени на убеждение компилятора в том, что вам можно запустить программу, и больше времени на её действительный запуск и работу с ней4). Последнее позволяет интерактивно разрабатываеть даже код обработки ошибок.

Другим следствием того, что Lisp — «программируемый язык программирования» является то, что, кроме возможности добавления в язык мелких изменений, которые позволяют легче жить, есть возможность сделать большие изменения относительно того, как язык должен работать. Например, оригинальная реализация Common Lisp Object System (CLOS) — объектной системы Common Lisp, была библиотекой, написанной на самом Common Lisp. Это даёт программистам возможность использования особенностей языка, которые пока отсутствуют в стандарте.

Какая бы новая парадигма программирования не появилась, Common Lisp, скорее всего, без труда сможет впитать её без изменений в ядре языка. Например, один программист на Lisp недавно написал библиотеку AspectL, которая добавляет Common Lisp поддержку аспектно-ориентированного программирования (AOP)5). Если будущее за AOP, то Common Lisp сможет поддерживать его без изменений в базовом языке и без дополнительных препроцессоров и прекомпиляторов6).
 
Как это началось?

Common Lisp – современный потомок языка программирования Lisp, придуманного Джоном Маккарти в 1956 году. Lisp был создан для «обработки символьных данных»7) и получил своё имя от одной вещи, в которой он был очень хорош: обработки списков (LISt Processing). Много воды утекло с тех пор, и теперь Common Lisp обогащён набором современных типов данных, которые вам только могут понадобиться, а также системой обработки ситуаций, которая, как вы увидите в главе 19, предоставляет уровень гибкости, отсутствующий в системах обработки исключений таких языков, как C++, Java, Python; мощной системой объектно-ориентированного программирования; несколькими особенностями, которых нет ни в одном другом языке. Как такое возможно? Что обусловило превращение Lisp в такой богатый язык?

Маккарти был (и есть) исследователем в области искусственного интеллекта, и многие особенности языка были заложены в него, начиная с самых ранних версий, делая его языком для программирования искусственного интеллекта. Во время бума ИИ в 80-е Lisp оставался излюбленным языком для решения сложных проблем, как то: автоматическое доказательство теорем, планирование и составление расписаний, компьютерное зрение. Это были проблемы, требующие сложных программ, для написания которых нужен был мощный язык, так что программисты ИИ сделали Lisp таковым. Во время Холодной войны Пентагон тратил деньги на Defense Advanced Research Projects Agency (DARPA), часть этих денег попадала к людям, занимающимся моделированием крупных сражений, автоматическим планированием и интерфейсами на естественных языках. Эти люди также использовали Lisp и продолжали совершенствовать его, чтобы язык полностью удовлетворял их потребностям.

Те же силы, что развивали Lisp, также расширяли границы и в других направлениях — сложные проблемы ИИ требуют больших вычислительных ресурсов, как бы вы их ни решали, и если вы примените закон Мура в обратном порядке, то сможете себе представить, сколь скудными эти ресурсы были в 80-е. Так что разработчики должны были найти все возможные пути улучшения производительности их реализаций языка. В результате этих усилий современные реализации Common Lisp часто включают в себя сложные компиляторы в язык, понятный машине. Хотя сегодня, благодаря закону Мура, возможно получить высокую производительность даже интерпретируемых языков, это больше не является проблемой для Common Lisp. И, как я покажу в главе 32, используя специальные (дополнительные) объявления, с помощью хорошего компилятора можно получить вполне приличный машинный код, сравнимый с тем, который выдаст компилятор C.

80-е — это также эра Lisp-машин. Несколько компаний, самая известная из которых Symbolics, выпускали компьютеры, которые могли запускать непосредственно Lisp-код на своих чипах. Так Lisp стал языком системного программирования, используемым для написания операционных систем, текстовых редакторов, компиляторов и много чего еще, что можно запустить на Lisp-машине.

Фактически, к началу 80-х существовало множество Lisp-лабораторий и несколько компаний, каждая со своей реализацией Lisp, их было так много, что люди из DARPA стали высказывать свои опасения о разобщённости Lisp-сообщества. Чтобы достигнуть единства, группа Lisp-хакеров собралась вместе и начала процесс стандартизации нового языка, Common Lisp, который бы впитал в себя лучшие черты существующих диалектов. Их работа запечатлена в книге Common Lisp the Language Гая Стила (Guy Steele, Digital Press, 1984) (CLtL).

К 1986 году существовало несколько реализаций стандарта, призванного заменить разобщённые диалекты. В 1996 организация The American National Standards Institute (ANSI) выпустила стандарт, расширяющий Common Lisp на базе CLtL, добавив в него новую функциональность, такую, как CLOS и систему обработки ситуаций. Но и это не было последним словом: как CLtL до этого, так и стандарт ANSI теперь целенаправленно позволяет разработчикам реализаций экспериментировать с тем, как лучше сделать те или иные вещи: реализация Lisp содержит богатую среду исполнения с доступом к ГИП FIXME, многопоточность, сокеты TCP/IP и многое другое. В наши дни Common Lisp эволюционирует, как и большинство других языков с открытым кодом: люди, использующие его, пишут библиотеки, которые им необходимы, и часто делают их доступными для всего сообщества. В последние годы, в частности, замечается усиление активности в разработке библиотек для Lisp.

Так что, с одной стороны, Lisp — один из классических языков в информатике (Computer Science), базирующийся на идеях, проверенных временем8). С другой стороны, Lisp — современный язык общего назначения, с дизайном, отражающим прагматический подход к решению сложных задач с максимальной надёжностью и эффективностью. Единственным недостатком наследия Lisp является множество предрассудков о нём, базирующихся на личном опыте его использования за последние 50 лет. Если кто-то говорит вам, что Lisp — только интерпретируемый язык, что он медленный, или что вы обязаны использовать рекурсию буквально для всего, спросите вашего оппонента, какой диалект Lisp'а имеется в видy, и носили ли люди клёш, когда он изучал Lisp9).
 
Но я изучал Lisp раньше, и он не был тем, что вы описываете!

Если вы изучали Lisp в прошлом, то можете подумать, что тот Lisp не имеет ничего общего с Common Lisp. Хотя Common Lisp вытеснил большинство диалектов, от которых он был порождён, это не единственный сохранившийся диалект, и, в зависимости от того, где и когда вы изучали Lisp, вы могли столкнуться с одним из них.

Кроме Common Lisp, активное сообщество пользователей есть у диалекта Lisp общего назначения под названием Scheme. Common Lisp позаимствовал из Scheme несколько важных особенностей, но никогда не пытался заменить его.

Разработанный в Массачуссетском Технологическом Институте (MIT), Scheme был быстро принят в качестве языка для начальных курсов по вычислительной технике. Scheme изначально занимал отдельную нишу, в частности, проектировщики языка постарались сохранить ядро Scheme настолько малым, насколько это возможно. Это давало очевидные выгоды при использовании Scheme как языка для обучения, а также при исследованиях в области языков программирования, так давало возможность формального доказательства предположений о языке.

Существовало также ещё одно преимущество: язык легко можно было изучить по спецификации. Все эти преимущества достигнуты засчёт отсутствия многих удобных особенностей, стандартизированных в Common Lisp. Конкретные реализации Scheme могут предоставлять эти возможности, но такие отклонения от стандарта делают написание переносимого кода на Scheme более сложным, чем на Common Lisp.
 
 
В Scheme гораздо большее внимание, чем в Common Lisp, уделяется функциональному стилю программирования и использованию рекурсии. Если вы изучали Lisp в университете и остались с впечатлением, что это академический язык без возможности применения в реальной жизни, существует вероятность, что вы изучали именно Scheme. Я не хочу сказать, что это правдивая характеристика Scheme, но это определение гораздо менее подходит для Common Lisp, который создавался для реальных инженерных задач, нежели для теоритезирования.
 
Также, если вы изучали Scheme, вас могут сбить с толку некоторые различия между Scheme и Common Lisp. Эти различия являются поводом непрекращающихся религиозных войн между горячими парнями, программирующими на этих диалектах. В данной книге я постараюсь указать на наиболее существенные различия.

Двумя другими распространёнными диалектами Lisp являются ELisp, язык расширений для редактора Emacs, и Autolisp, язык расширений для программы Autodesk AutoCAD. Хотя, возможно, суммарный объём кода, написанного на этих диалектах, перекрывает весь остальной код, написанный на Lisp, оба эти диалекта могут использоваться только в рамках приложений, которые они расширяют. Кроме того, они являются устаревшими по сравнению и с Common Lisp, и с Scheme. Если Вы использовали один из этих диалектов, приготовьтесь к путешествию на Lisp-машине времени на несколько десятилетий вперёд.
 
Намылить, смыть, повторить: знакомство с REPL

В этой главе вы настроите среду программирования и напишете свои первые программы на Common Lisp. Мы воспользуемся лёгким в установке дистрибутивом Lisp in a Box, разработанным Matthew Danish и Mikel Evins, включающим в себя реализацию Common Lisp, мощным, прекрасно поддерживающим Lisp текстовым редактором Emacs, а также SLIME) — средой разработки на Common Lisp, основанной на Emacs.

Этот набор предоставляет программисту современную среду разработки на Common Lisp, поддерживающую инкрементальный, интерактивный стиль разработки, характерный для программирования на этом языке. Среда SLIME даёт дополнительное преимущество в виде унифицированного пользовательского интерфейса, не зависящего от выбранных вами операционной системы и реализации Common Lisp. В своей книге я буду ориентироваться на среду Lisp in a Box, но те, кто хочет изучить другие среды разработки, например, графические интегрированные среды разработки (IDE - Integrated Development Environment), предоставляемые некоторыми коммерческими поставщиками, или среды, основанные на других текстовых редакторах, не должны испытывать больших трудностей в понимании.
 
Выбор реализации Lisp
 
Первое, что вам предстоит сделать — выбрать реализацию Lisp. Это может показаться немного странным тем, кто раньше занимался программированием на таких языках как Perl, Python, Visual Basic (VB), C# или Java. Разница между Common Lisp и этими языками заключается в том, что Common Lisp определяется своим стандартом: не существует ни единственной его реализации, контролируемой «великодушным диктатором» (как в случае с Perl и Python), ни канонической реализации, контролируемой одной компанией (как в случае с VB, C# или Java). Любой желающий может создать свою реализацию на основе стандарта. Кроме того, изменения в стандарт должны вноситься в соответствии с процессом, контролируемым Американским Национальным Институтом Стандартов (ANSI). Этот процесс организован таким образом, что «случайные лица», такие, как частные поставщики программных решений, не могут носить изменения в стандарт по своему усмотрению. Таким образом, стандарт Common Lisp — это договор между поставщиком Common Lisp и использующими Common Lisp разработчиками; этот договор подразумевает, что, если вы пишете программу, использующую возможности языка так, как это описано в стандарте, вы можете рассчитывать, что эта программа запустится на любой совместимой реализации Common Lisp.

С другой стороны, стандарт может описывать не всё из того, что вам может понадобиться в ваших программах. Более того, на некоторые аспекты языка спецификация намеренно отсутствует, чтобы дать возможность поэксперементировать с различными способами их реализации, если при разработке стандарта не было достигнуто договорённости о наилучшем способе. Как видите, каждая реализация предоставляет пользователям как входящие в стандарт возможности, так и возможности, выходящие за его пределы. В зависимости от того, программированием какого рода вы собираетесь заняться, вы можете выбрать реализацию Common Lisp, поддерживающую именно те дополнительные возможности, которые вам больше всего понадобятся. С другой стороны, если вы предоставите другим разработчикам пользоваться вашим кодом на Lisp, например, разработанными вами библиотеками, вы, вероятно, захотите — конечно, в пределах возможного — написать переносимую реализацию Common Lisp. Для нужд написания кода, который должен быть переносимым, но, в тоже время, использовать возможности, не описанные в стандарте, Common Lisp предоставляет гибкий способ писать код, «зависящий» от возможностей текущей реализации. Вы увидите пример такого кода в главе 15, когда мы будем разрабатывать простую библиотеку, «сглаживающую» некоторые различия в обработке разными реализациями Lisp имён файлов.

Сейчас, однако, наиболее важная характиристика реализации — её способность работать в вашей любимой операционной системе. Сотрудники компании Franz, занимающейся разработкой Allegro Common Lisp, выпустили пробную версию своего продукта, предназначенного для использования с этой книгой, и выполняющегося на GNU/Linux, Windows и OS X. У читателей, предпочитающих реализации с открытыми исходными текстами, есть несколько вариантов. SBCL - высококачественная открытая реализация, способная компилировать в машинный код и работать на множестве различных UNIX-систем, включая Linux и OS X. SBCL — «наследник» CMUCL5) — реализации Common Lisp, разработанной в университете Carnegie Mellon, и, как и CMUCL, является всеобщим достоянием (public domain, за исключением нескольких секций, покрываемых BSD-подобными (Berkley Software Distributions) лицензиями). CMUCL — тоже хороший выбор, однако SBCL, обычно, легче в установке и поддерживает 21-разрядный Unicode6). OpenMCL будет отличным выбором для пользователей OS X: эта реализация способна компилировать в машинный код, поддерживать работу с потоками, а также прекрасно интегрируется с инструментальными комплектами Carbon и Coca. Кроме перечисленных, существуют и другие свободные и коммерческие реализации. Если вы захотите получить больше информации, в главе 32 вы найдёте список ресурсов.

Весь код на Lisp, приведённый в этой книге, должен работать на любой совместимой реализации Common Lisp, если явно не указано обратное, и SLIME будет «сглаживать» некоторые различия между реализациями, предоставляя общий интерфейс для взаимодействия с Lisp. Сообщения интерпретатора, приведённые в этой книге, сгенерированы Allegro, запущеном на GNU/Linux. В некоторых случаях другие реализации Lisp могут генерировать сообщения, незначительно отличающиеся от приведённых.
 
Введение в Lisp in a Box

Lisp in a Box спроектирован с целью быть «дружелюбным» к Лисперам-новичкам и предоставлять первокласную среду разработки на Lisp с минимальными усилиями, и потому всё что вам нужно для работы - это взять соответствующий пакет для вашей операционной системы и выбранную вами реализацию Lisp с веб-сайта Lisp in a Box (см. главу 32) и далее следовать инструкциям по установке.

Так как Lisp in a Box использует Emacs в качестве текстового редактора, вы должны хоть немного уметь им пользоваться. Возможно, лучший способ начать работать с Emacs - это изучать его по встроенному учебнику (tutorial). Чтобы вызвать tutorial, выбирете первый пункт меню Help – Emacs tutorial. Или же зажмите Ctrl и нажмите h, затем отпустите Ctrl и нажмите t. Большенство команд в Emacs доступно через комбинации клавиш, поэтому они будут встречаться довольно часто, и чтобы долго не описывать комбинации (например: «зажмите Ctrl и нажмите h, затем…»), в Emacs существует краткая форма записи комбинаций клавиш. Клавиши, которые должны быть нажаты вместе, пишутся вместе, разделяются тире, и называются связками; связки разделяются пробелами. C обозначает Ctrl, а M - Meta (Alt). Например вызов tutorial будет выглядеть таким образом: C-h t.

Tutorial также описывает много других полезных команд Emacs и вызывающих их комбинаций клавиш. У Emacs также есть расширенная онлайн документация, для просмотра которой используется специальный браузер – Info. Чтобы её вызвать нажмите C-h i. У Info также есть своя справка, которую можно вызвать, нажав клавишу h, находясь в браузере Info. Emacs предоставляет ещё несколько способов получить справку – это все сочетания клавиш, начинающиеся с C-h – полный список по C-h ?. В этом списке есть две полезные вещи: C-h k «объяснит» комбинацию клавиш, а C-h w – команду.
 
   
Ещё одна важная часть терминологии (для тех, кто отказался от работы с tutorial) - это буфер. Во время работы в Emacs, каждый файл, который Вы редактируете, представлен в отдельном буфере. Только один буфер может быть «текущим» в любой момент времени. В текущий буфер поступает весь ввод – всё, что Вы печатаете и любые команды, которые вызываете. Буферы также используются для представления взаимодействия с программами (например с Common Lisp). Есть одна простая вещь, которую вы должны знать – «переключение буферов», означающее смену текущего буфера, так что Вы можете редактировать определённый файл или взаимодействовать с определённой программой. Команда switch-to-buffer, привязанная к комбинации клавиш C-x b, запрашивает имя буфера (в нижней части окна Emacs). Во время ввода имени буфера, Вы можете пользоваться автодополнением по клавише Tab, которое по начальным символам завершает имя буфера или выводит список возможных вариантов. Просто нажав ввод, Вы переключитесь в буфер «по-умолчанию» (таким-же образом и обратно). Вы также можете переключать буферы, выбирая нужный пункт в меню Buffers.

В определенных контекстах для переключения на определенные буферы могут быть доступны другие комбинации клавиш. Например, при редактировании исходных файлов Lisp сочетание клавиш C-c C-z переключает на буфер, в котором вы взаимодействуете с Lisp.
 
Освободите свой разум: Интерактивное програмирование

При запуске Lisp in a Box, вы должны увидеть приглашение, которое может выглядеть примерно так :
 
CL-USER>

Это приглашение Lisp. Как и приглашение оболочки DOS или UNIX, приглашение Lisp это место куда вы можете печатать выражения, которые заставляют что-либо делать компьютер. Однако, вместо того чтобы считывать и выполнять строку команд оболочки, Lisp считывает Lisp выражения, вычисляет их согласно правилам Lisp, и печатает результат. Потом он (lisp) повторяет свои действия со следующим введенном вами выражением. Вот вам бесконечный цикл: считывания, вычисления, и печати(вывода на экран), поэтому он называется цикл-чтение-вычисление-печать (по-английски read-eval-print-loop), или сокращённо REPL . Этот процесс может также называться top-level, top-level listener, или Lisp listener.
 
Через окружение, предоставленное REPL'ом, вы можете определять и переопределять элементы программ такие как переменные, функции, классы и методы; вычислять выражения Lisp; загружать файлы, содержащие исходные тексты Lisp или скомпилированные программы; компилировать целые файлы или отдельные функции; входить в отладчик; пошагово выполнять программы; и проверять состояние отдельных объектов Lisp;

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

Для знакомства с REPL, вам необходимо выражение Lisp, которое может быть прочитано, вычислено и выведено на экран. Простейшее выражение Lisp - это число. Если вы наберете 10 в приглашении Lisp и нажмете ВВОД, то сможете увидите что-то наподобие: 
 
CL-USER> 10
10
 
Первая 10 - это то, что вы набрали. Считыватель Lisp, R в REPL, считывает текст «10» и создаёт объект Lisp, представляющий число 10. Этот объект - самовычисляемый объект, это означает, что такой объект при передаче в вычислитель, E в REPL, вычисляется сам в себя. Это значение подаётся на принтер, который напечатает объект «10» в отдельной строке. Хотя это и похоже на сизифов труд, можно получить что-то поинтереснее если дать интерпретатору Lisp пищу для размышлений. Например, вы можете набрать (+ 2 3) в приглашение Lisp.
 
CL-USER> (+ 2 3)
5
 
Все что в скобках - это список, в данном случае список из трех элементов, символ +, и числа 2 и 3. Lisp, в общем случае, вычисляет списки считая первый элемент именем функции и остальные - выражениями для вычисления и передачи в качестве аргументов этой функции. В нашем случае, символ + - название функции которая вычисляет сумму. 2 и 3 вычисляются сами в себя и передаются в функцию суммирования, которая возвращает 5. Значение 5 отправляется на устройство вывода, которое отображает его. Lisp может вычислять выражения и другими способами, но не будем сильно отдаляться от основной темы. В первую очередь вы должны написать...
 
"Здравствуй, Мир" в стиле Lisp

Нет законченной книги по программированию без программы «Здравствуй, мир»(«hello, world.»)8). После того как интерпретатор запущен, нет ничего проще чем набрать строку «Здравствуй, мир». 
 
CL-USER> "Здравствуй, мир"
"Здравствуй, мир"
 
Это работает поскольку строки, также как и числа, имеют символьный синтаксис понимаемый считывателем Lisp и являются самовычисляемыми объектами: Lisp считывает строку в двойных кавычках и создает в памяти строковой объект, который при вычислении вычисляется сам в себя и потом печатается в том же символьном представлении. Кавычки не являются частью строкового объекта в памяти - это просто синтаксис, который позволяет считывателю определить что этот объект - строка. Принтер также выводит кавычки на вывод, потому что он пытается выводить объекты в таком же виде, в каком понимает их считыватель.

Однако, наш пример не может квалифицироваться как программа «Здравствуй мир». Это, скорее, значение «Здравствуй мир».
 
 
Вы можете сделать шаг к настоящей программе, напечатав код, который в качестве побочного эффекта выведет на стандартный вывод строку «Здравствуй, мир». Common Lisp предоставляет несколько путей для вывода данных, но самый гибкий - это функция FORMAT. FORMAT получает переменное количество параметров, но только два из них обязательны: указание, куда осуществлять вывод, и строка для вывода. В следующей главе Вы увидите, как строка может содержать встроенные директивы, которые позволяют вставлять в строку последующие параметры функции, а-ля printf или строку-% из Python. До тех пор, пока строка не содержит символа ~, она будет выводиться как есть. Если вы передадите t в качестве первого параметра, функция FORMAT направит свой вывод на стандартный вывод. Итак, выражение FORMAT для печати «Здравствуй, мир» выглядит примерно так:
 
CL-USER> (format t "Здравствуй, мир")
Здравствуй, мир
NIL
 
Стоит заметить, что результатом выражения FORMAT является NIL в строке после вывода «Здравствуй, мир». Этот NIL является результатом вычисления выражения FORMAT, напечатанного REPL. (NIL – это Lisp-версия false и/или null. Подробнее об этом рассказывается в главе 4.) В отличие от других выражений, рассмотренных ранее, нас больше интересует побочный эффект выражения FORMAT (в данном случае, печать на стандартный вывод), чем возвращаемое им значение. Но каждое выражение в Lisp вычисляется в некоторый результат.

Однако, до сих пор остается спорным, написали ли мы настоящую программу. Но вы ведь здесь. Вы видите восходящий стиль программирования, поддерживаемый REPL: вы можете можете экспериментировать с различными подходами и строить решения из уже протестированных частей. Теперь, когда у вас есть простое выражение, которое делает то, что вы хотите, нужно просто упаковать его в функцию. Функции являются одним из основных строительных материаллов в Lisp и могут быть определены с помощью выражения DEFUN подобным образом: 
 
CL-USER> (defun hello-world () (format t "hello, world"))
HELLO-WORLD
 
Выражение hello-world, следующее за DEFUN является именем функции. В главе 4 мы рассмотрим, какие именно символы могут использоваться в именах, но сейчас будет достаточно сказать, что многие символы, такие как «-», которые нелегальны в именах в других языках абсолютно легальны в Common Lisp. Это стандартный стиль «Lisp – not to mention more in line with normal English typography» – формирование составных имен с помощью дефисов, как в hello-world, вместо использования знаков подчеркивания, как в hello_world, или использованием заглавных букв внутри имени, как helloWorld. Скобки () после имени отделяют список параметров, который в данном случае пуст, так как функция не принимает аргументов. Остальное - это тело функции.

На одном уровне это выражение, подобно всем другим, которые вы видели, всего лишь еще одно выражение для чтения, вычисления и печати, осуществляемых REPL. Возвращаемое значение в этом случае - это имя только что определенной функции. Но, подобно выражению FORMAT, это выражение более интересно своими побочными эффектами, нежели возвращаемым значением. Однако, в отличие от выражения FORMAT, побочные эффекты невидимы: после вычисления этого выражения создается новая функция, не принимающая аргументов, с телом (format t «hello, world») и ей дается имя HELLO-WORLD.

Теперь, после определения функции, вы можете вызвать ее следующим образом: 
 
CL-USER> (hello-world)
hello, world
NIL
 
Вы можете видеть, что вывод в точности такой же, как при вычислении выражения FORMAT напрямую, включая значение NIL, напечатанное REPL. Функции в Common Lisp автоматически возвращают значение последнего вычисленного выражения.
 
Сохранение вашей работы

Вы могли бы утверждать, что это готовая программа «hello, world». Однако, остаётся одна проблема. Если вы выйдете из Lisp и перезапустите его, определение функции исчезнет. Имея такую изящную функцию, вы захотите сохранить вашу работу.

Это достаточно просто. вы просто должны создать файл, в котором сохраните определение. В Emacs вы можете создать новый файл, набрав C-x C-f и затем, когда Emacs выведет подсказку, введя имя файла, который вы хотите создать. Не особенно важно, где будет находиться этот файл. Обычно исходные файлы Common Lisp именуются с расширением .lisp, хотя некоторые люди предпочитают .cl вместо него.

Открыв файл, вы можете набирать определение функции, введённое ранее в области REPL. Обратите внимание, что после набора открывающей скобки и слова DEFUN, в нижней части окна Emacs SLIME подскажет вам предполагаемые аргументы. Точная форма зависит от используемой вами реализации Common Lisp, но вы вероятно увидите что-то вроде этого: 
 
(defun name varlist &rest body)
 
Сообщение будет исчезать, когда вы будете начинать печатать каждый новый элемент, и снова появляться после ввода пробела. При вводе определения в файл вы можете захотеть разбить определение после списка параметров так, чтобы оно занимало две строки. Если вы нажмете Enter, а затем Tab, SLIME автоматически выровняет вторую строку соответствующим образом:
 
(defun hello-world ()
(format t "hello, world"))
 
SLIME также поможет вам в согласовании скобок – как только вы наберете закрывающую скобку, SLIME подсветит соответствующую открывающую скобку. Или вы можете просто набрать C-c C-q для вызова команды slime-close-parens-at-point, которая вставит столько закрывающих скобок, сколько нужно для согласования со всем открытыми скобками.
 
 
Теперь вы можете отправить это определение в вашу среду Lisp несколькими способами. Самый простой - это набрать C-c C-c, когда курсор находится где-нибудь внутри или сразу после формы DEFUN, что вызовет команду slime-compile-defun, которая, в свою очередь, пошлет определение в Lisp для вычисления и компиляции. Для того, чтобы убедиться, что это работает, вы можете сделать несколько изменений в hello-world, перекомпилировать ее, а затем вернуться назад в REPL, используя C-c C-z или C-x b, и вызвать ее снова. Например, вы можете сделать эту функцию более грамматически правильной.
 
(defun hello-world ()
(format t "Hello, world!"))
 
Теперь перекомпилируем ее с помощью C-c C-c и перейдем в REPL, набрав C-c C-z, чтобы попробовать новую версию.
 
CL-USER> (hello-world)
Hello, world!
NIL
 
Теперь вы возможно захотите сохранить файл, с которым работаете; находясь в буфере hello.lisp, наберите C-x C-s для вызова функции Emacs save-buffer.

Теперь, для того, чтобы попробовать перезагрузить эту функцию из файла с иходным кодом, вы должны выйти из Lisp и перезапустить его. Для выхода вы можете использовать клавишную комбинацию SLIME: находясь в REPL, наберите запятую. Внизу окна Emacs вам будет предложена ввести команду. Наберите quit (или sayoonara), а затем нажмите Enter. Произойдет выход из Lisp, а все окна, созданные SLIME (такие как буфер REPL), закроются13). Теперь перезапустите SLIME, набрав M-x slime.

Просто ради интереса, вы можете попробовать вызвать hello-world. 
 
CL-USER> (hello-world)
 
После этого возникнет новый буфер SLIME, содержимое которого будет начинаться с чего-то вроде этого: 
 
attempt to call `HELLO-WORLD' which is an undefined function.
   [Condition of type UNDEFINED-FUNCTION]

Restarts:
   0: [TRY-AGAIN] Try calling HELLO-WORLD again.
   1: [RETURN-VALUE] Return a value instead of calling HELLO-WORLD.
   2: [USE-VALUE] Try calling a function other than HELLO-WORLD.
   3: [STORE-VALUE] Setf the symbol-function of HELLO-WORLD and call it again.
   4: [ABORT] Abort handling SLIME request.
   5: [ABORT] Abort entirely from this process.

Backtrace:
  0: (SWANK::DEBUG-IN-EMACS #<UNDEFINED-FUNCTION @ #x716b082a>)
  1: ((FLET SWANK:SWANK-DEBUGGER-HOOK SWANK::DEBUG-IT))
  2: (SWANK:SWANK-DEBUGGER-HOOK #<UNDEFINED-FUNCTION @ #x716b082a> #<Function SWANK-DEBUGGER-HOOK>)
  3: (ERROR #<UNDEFINED-FUNCTION @ #x716b082a>)
  4: (EVAL (HELLO-WORLD))
  5: (SWANK::EVAL-REGION "(hello-world)
" T)
 
Что же произошло? Просто вы попытались вызвать функцию, которая не существует. Но не смотря на такое количество выведенной информации, Lisp на самом деле обрабатывает такую ситуацию изящно. В отличие от Java или Python, Common Lisp не просто генерирует исключение и разворачивает стек. И он точно не завершается, оставив после себя образ памяти (dump core), только потому, что вы попытались вызвать несуществующую функцию. Вместо этого он перенесет вас в отладчик.

Во время работы с отладчикам вы все еще имеете полный доступ к Lisp, поэтому вы можете вычислять выражения для исследования состояния вашей программы и может быть даже для исправления каких-то вещей. Сейчас не стоит беспокоиться об этом; просто наберите q для выхода из отладчика и возвращения назад в REPL. Буфер отладчика исчезнет, а REPL выведет следующее: 
 
CL-USER> (hello-world)
Evaluation aborted
CL-USER>
 
Конечно, в отладчике можно сделать гораздо больше, чем просто выйти из него – в главе 19 мы увидим, например, как отладчик интегрируется с системой обработки ошибок. А сейчас, однако, важной вещью, которую нужно знать, является то, что вы всегда можете выйти из отладчика и вернуться обратно в REPL, набрав q.
 
 
Вернувшись в REPL вы можете попробовать снова. Ошибка произошла, потому что Lisp не знает определения hello-world. Поэтому вам нужно предоставить Lisp определение, сохраненное нами в файле hello.lisp. Вы можете сделать это несколькими способами. Вы можете переключиться назад в буфер, содержащий файл (наберите C-x b, а затем введите hello.lisp) и перекомпилировать определение, как вы это делали ранее с помощью C-c C-c. Или вы можете загрузить файл целиком (что будет более удобным способом, если файл содержит множество определений) путем использования функции LOAD в REPL следующим образом: 
 
CL-USER> (load "hello.lisp")
; Loading /home/peter/my-lisp-programs/hello.lisp
T
 
T означает, что загрузка всех определений произошла успешно14). Загрузка файла с помощью LOAD в сущности эквивалентна набору каждого выражения этого файла в REPL в том порядке, в каком они находятся в файле, таким образом, после вызова LOAD, hello-world должен быть определен.
 
CL-USER> (hello-world)
Hello, world!
NIL
 
Еще один способ загрузки определений файла - предварительная компиляция файла с помощью COMPILE-FILE, а затем загрузка (с помощью LOAD) уже скомпилированного файла, называемого FASL-файлом, что является сокращением для fast-load file (быстро загружаемый файл). COMPILE-FILE возвращает имя FASL-файла, таким образом мы можем скомпилировать и загрузить файла из REPL следующим образом:
 
CL-USER> (load (compile-file "hello.lisp"))
;;; Compiling file hello.lisp
;;; Writing fasl file hello.fasl
;;; Fasl write complete
; Fast loading /home/peter/my-lisp-programs/hello.fasl
T
 
SLIME также предоставляет возможность загрузки и компиляции файлов без использования REPL. Когда вы находитесь в буфере с исходным кодом, вы можете использовать C-c C-l для загрузки файла с помощью slime-load-file. Emacs выведет запрос имени файла для загрузки с уже введенным именем текущего файла; вы можете просто нажать Enter. Или же вы можете набрать C-c C-k для компиляции и загрузки файла, представляемого текущим буфером. В некоторых реализациях Common Lisp компилирование кода таким образом выполнится немного быстрее; в других - нет, обычно потому что они всегда компилируют весь файл целиком.

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

Кроме того, даже если приложение, написанное на Lisp, уже развернуто, часто существует возможность обратиться к REPL. В главе 26 вы увидите как можно использовать REPL и SLIME для взаимодействия с Lisp, запустившим Web-сервер, в то же самое время, когда он продолжает отдавать Web-страницы. Возможно даже использовать SLIME для соединения с Lisp, запущенным на другой машине, что позволяет, например, отлаживать удаленный сервер так же, как локальный.

И даже более впечатляющий пример удаленной отладки произошел в миссии NASA «Deep Space 1» в 1998 году. Через полгода после запуска космического корабля, небольшой код на Lisp должен был управлять космическим кораблем в течении двух дней для проведения серии экспериментов. Однако, неуловимое состояние гонки (race condition) в коде не было выявлено при тестировании на земле и было обнаружено уже в космосе. Когда ошибка была выявлена в космосе (100 миллионов миль от Земли) команда смогла произвести диагностику и исправление работающего кода, что позволило завершить эксперимент15). Один из программистов сказал об этом следующее:

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

Вы пока не готовы отправлять какой бы то ни было код Lisp в дальний космос, но в следующей главе вы напишите программу, которая немного более интересна, чем «hello, world». 
 
Практикум: Простая база данных

Очевидно, перед тем, как создавать настоящие программы на Lisp, вам необходимо изучить язык. Но давайте смотреть правде в глаза — вы можете подумать «Practical Common Lisp? Похоже на оксюморон. Зачем тратить силы на изучение деталей языка, если на нем невозможно сделать что-то дельное?». Итак, я начну с маленького примера на Common Lisp, который вы можете попробовать сами. В этой главе вы напишете простую базу данных для хранения CD-треков. В 27 главе вы будете использовать схожую технику при создании базы данных записей в формате MP3 для вашего потокового MP3-сервера. Фактически можете считать это частью вашего программного проекта – в конце концов, для того, чтобы иметь сколько-нибудь MP3-записей для прослушивания, совсем не помешает знать, какие записи у вас есть, а какие нужно извлечь с диска.

В этой главе я пройдусь по языку Lisp достаточно для того, чтобы вы продвинулись до понимания, каким образом работает код на нём. Но я не буду вдаваться в детали. Вы можете не беспокоиться, если что-то здесь будет вам непонятно — в нескольких следующих главах все (и даже больше) используемые здесь конструкции Common Lisp будут описаны гораздо более систематически.
 
 
Одно замечание по терминологии: в этой главе я расскажу о некоторых операторах Lisp. В главе 4 вы узнаете, что Common Lisp предоставляет три разных типа операторов: функции, макросы и специальные операторы. Для целей этой главы вам необязательно понимать разницу. Однако я буду ссылаться на различные операторы как на функции, макросы или специальные операторы, в зависимости от того, чем они на самом деле являются, вместо того, чтобы попытаться скрыть эти детали за одним словом – оператор. Сейчас вы можете рассматривать функции, макросы и специальные операторы как более или менее эквивалентные сущности.

Также имейте ввиду, что я не буду использовать все наиболее сложные техники Common Lisp для вашей первой после «Hello, world» программы. Целью этой главы является не то, как вы можете написать базу данных на Lisp; цель — дать вам понимание того, чем хорошо программирование на Lisp и видение того, что даже относительно простая программа на Lisp может иметь много возможностей.
 
CD и Записи

Для хранения данных о дорожке, которая должна быть перекодирована в MP3, и том, какой CD должен быть перекодирован в первую очередь, каждая запись в базе данных будет содержать название и имя исполнителя компакт диска, оценка того, насколько он нравится пользователю, и флаг, указывающий, был ли диск уже перекодирован. Итак, для начала вам необходим способ представления одной записи в базе данных (другими словами, одного CD). Common Lisp предоставляет для этого много различных структур данных, от простого четырехэлементного списка до определяемого пользователем с помощью CLOS класса данных.

Для начала вы можете остановиться на простом варианте и использовать список. Вы можете создать его с помощью функции LIST, которая, соответственно, возвращает список из переданных аргументов. 
 
CL-USER> (list 1 2 3)
 (1 2 3)
 
Вы могли бы использовать четырёхпозиционный список, отображающий позицию в списке на соответствующее поле записи. Однако другая существующая разновидность списков, называемая property list (список свойств) или, сокращенно, plist, в нашем случае гораздо удобнее. Plist — это такой список, в котором каждый нечетный элемент является символом, описывающим следующий (чётный) элемент списка. На этом этапе я не буду углубляться в подробности понятия символ; по своей природе это имя. Для символов, именующих поля в базе данных, мы можем использовать частный случай символов, называемый символами-ключами (keyword symbol). Ключ — это имя, начинающееся с двоеточия (:), например, :foo. Вот пример plist, использующего символы-ключи :a, :b и :c как имена свойств: 
 
CL-USER> (list :a 1 :b 2 :c 3)
(:A 1 :B 2 :C 3)
 
Заметьте, вы можете создать список свойств той же функцией LIST, которой создавали прочие списки. Характер содержимого — вот что делает его списком свойств.

Причина, по который использование plist является предпочтительным — наличие функции GETF, в которую передают plist и желаемый символ и получают значение следующего за символом значения. Это делает plist чем-то вроде упрощенной хэш-таблицы. В Lisp есть и «настоящие» хэш-таблицы, но для ваших текущих нужд достаточно plist, к тому же намного проще сохранять данные в такой форме в файл, это сильно пригодится позже. 
 
CL-USER> (getf (list :a 1 :b 2 :c 3) :a)
1
CL-USER> (getf (list :a 1 :b 2 :c 3) :c)
3
 
Теперь, зная это, вам будет достаточно просто написать функцию make-cd, которая получит четыре поля в качестве аргументов и вернёт plist, представляющий CD. 
 
(defun make-cd (title artist rating ripped)
  (list :title title :artist artist :rating rating :ripped ripped))
 
Слово DEFUN говорит нам, что это запись определяет новую функцию. Имя функции make-cd. После имени следует список параметров. Функция содержит четыре параметра — title, artist, rating и ripped. Всё, что следует за списком параметров — тело функции. В данном случае тело — лишь форма, просто вызов функции LIST. При вызове make-сd параметры, переданные при вызове, будут связаны с переменными в списке параметров из объявления функции. Например, для создания записи о CD Roses от Kathy Mattea вы можете вызвать make-cd примерно так: 
 
CL-USER> (make-cd "Roses" "Kathy Mattea" 7 t)
 (:TITLE "Roses" :ARTIST "Kathy Mattea" :RATING 7 :RIPPED T)
 
Заполнение CD

Впрочем, создание одной записи — ещё не создание базы данных. Вам необходима более комплексная структура данных для хранения записей. Опять же, простоты ради, список представляется здесь вполне подходящим выбором. Также для простоты вы можете использовать глобальную переменную *db*, которую можно будет определить с помощью макроса DEFVAR. Звездочка (*) в имени переменной — это договоренность, принятая в языке Lisp при объявлении глобальных переменных:
 
(defvar *db* nil)
 
Вы можете использовать макрос PUSH для добавления элементов в *db*. Но, возможно, неплохой идеей будет немного абстрагировать вещи и определить функцию 'add-record', котороя будет добавлять записи в базу данных.
 
(defun add-record (cd) (push cd *db*))
 
Теперь вы можете использовать add-record вместе с make-cd для добавления CD в базу данных.
 
CL-USER> (add-record (make-cd "Roses" "Kathy Mattea" 7 t))
 ((:TITLE "Roses" :ARTIST "Kathy Mattea" :RATING 7 :RIPPED T))
 CL-USER> (add-record (make-cd "Fly" "Dixie Chicks" 8 t))
 ((:TITLE "Fly" :ARTIST "Dixie Chicks" :RATING 8 :RIPPED T)
  (:TITLE "Roses" :ARTIST "Kathy Mattea" :RATING 7 :RIPPED T))
 CL-USER> (add-record (make-cd "Home" "Dixie Chicks" 9 t))
 ((:TITLE "Home" :ARTIST "Dixie Chicks" :RATING 9 :RIPPED T)
  (:TITLE "Fly" :ARTIST "Dixie Chicks" :RATING 8 :RIPPED T)
  (:TITLE "Roses" :ARTIST "Kathy Mattea" :RATING 7 :RIPPED T))
 
Всё, что REPL выводит после каждого вызова add-record — значения, возвращаемые последним выражением в теле функции, в нашем случае - PUSH. А PUSH возвращает новое значение изменяемой им переменной. Таким образом, после каждого добавления порции данных вы видите содержимое вашей базы данных.
 
Просмотр содержимого базы данных

Вы также можете просмотреть текущее значение *db* в любой момент, набрав *db* в REPL. 
 
CL-USER> *db*
((:TITLE "Home" :ARTIST "Dixie Chicks" :RATING 9 :RIPPED T)
 (:TITLE "Fly" :ARTIST "Dixie Chicks" :RATING 8 :RIPPED T)
  (:TITLE "Roses" :ARTIST "Kathy Mattea" :RATING 7 :RIPPED T))
 
Правда, это не лучший способ просмотра данных. Вы можете написать функцию dump-db, которая выводит содержимое базы данных в более удобной форме, например, так: 
 
 TITLE:    Home
 ARTIST:   Dixie Chicks
 RATING:   9
 RIPPED:   T
 
 TITLE:    Fly
 ARTIST:   Dixie Chicks
 RATING:   8
 RIPPED:   T
 
 TITLE:    Roses
 ARTIST:   Kathy Mattea
 RATING:   7
 RIPPED:   T
 
Эта функция может выглядить так: 
 
(defun dump-db ()
  (dolist (cd *db*)
    (format t "~{~a:~10t~a~%~}~%" cd)))
 
Работа функции заключается в циклическом обходе всех элементов *db* с помощью макроса DOLIST, связывая на каждой итерации каждый элемент с переменной cd. Для вывода на экран каждого значения cd используется функция FORMAT.

Следует признать, вызов FORMAT выглядит немного загадочно. Но в действительности FORMAT неособенно сложнее, чем функция printf из С или Perl или оператор % из Python. В главе 18 я расскажу о FORMAT более подробно. Теперь же давайте шаг за шагом рассмотрим, как работает этот вызов. Как было показано в гл. 2, FORMAT принимает по меньшей мере два аргумента, первый из которых - поток, в который FORMAT направляет свой вывод; t — сокращённое обозначение потока *standard-output*.
 
  
Второй аргумент FORMAT — формат строки; он может как содержать символьный текст, так и управляющие команды, контроллирующие работу этой функции, например то, как она должна интерпретировать остальные аргументы. Команды, управляющие форматом вывода, начинаются со знака тильды (~) (так же, как управляющие команды начинаются с %). FORMAT может принимать довольно много таких команд, каждую со своим набором параметров. Однако сейчас я сфокусируюсь только на тех управляющих командах, которые необходимы для написания функции dump-db.

Команда ~a служит для придания выводимым строкам некоторой эстетичности. Она принимает аргумент и возвращает его в удобочитаемой форме. Эта команда отобразит ключевые слова без предваряющего : и строки без кавычек. Например: 
 
CL-USER> (format t "~a" "Dixie Chicks")
Dixie Chicks
NIL
 
или: 
 
CL-USER> (format t "~a" :title)
TITLE
NIL
 
Команда ~t предназначена для табулирования. Например, ~10t указывает FORMAT, что необходимо выделить достаточно места для перемещения в десятый столбец перед выполнением команды ~a. ~t не принимает аргументов. 
 
CL-USER> (format t "~a:~10t~a" :artist "Dixie Chicks")
ARTIST:   Dixie Chicks
NIL
 
Теперь рассмотрим немного более сложные вещи. Когда FORMAT обнаруживает ~{, следующим аргументом должен быть список. FORMAT циклично просматривает весь список, на каждой итерации выполняя команды между ~{ и ~} и используя столько элементов списка, сколько нужно для вывода согласно этим командам. В функции dump-db FORMAT будет циклично просматривать список и на каждой итерации принимать одно ключевой слово и одно значение списка. Команда ~% не принимает аргументов, но заставляет FORMAT выполнять переход на новую строку. После выполнения команды ~} итерация заканчивается, и последняя ~% заставляет FORMAT сделать ещё один переход на новую строку, чтобы записи, соответствующие каждому CD, были разделены. Формально, вы также можете использовать FORMAT для вывода именно базы данных, сократив тело функции dump-db до одной строки. 
 
(defun dump-db ()
  (format t "~{~{~a:~10t~a~%~}~%~}" *db*))
 
Это может показаться довольно элегантным или, напротив, грубым приёмом, в зависимости от вашего мнения на этот счёт. 
 
Улучшение взаимодействия с пользователем

Хотя функция add-record прекрасно выполняет свои обязанности, она слишком необычна для пользователя, незнакомого с Lisp. И если он захочет добавить в базу данных несколько записей, это может показаться ему довольно неудобным. Значит, придётся написать функцию, запрашивающую информацию о нескольких CD. Теперь вы уже знаете, как запросить у пользователя информацию и считать её, поэтому мы можем воспользоваться следующим кодом: 
 
(defun prompt-read (prompt)
 (format *query-io* "~a: " prompt)
 (force-output *query-io*)
 (read-line *query-io*))
 
Мы использовали уже знакомую нам функцию FORMAT, чтобы вывести приглашение. Заметим, что в строке, описывающей формат, отсутствует «~%», поэтому перевода курсора на новую строку не происходит. Вызов FORCE-OUTPUT необходим в некоторых реализациях для уверенности в том, что Lisp не будет ожидать вывода новой строки перед выводом приглашения.

Теперь прочитаем одну строку текста с помощью (очень удачно названной!) функции READ-LINE. Переменная *QUERY-IO* является глобальной (о чем можно догадаться по наличию в её имени символов *), она содержит входной поток, связанный с терминалом. Значение, возвращаемое функцией PROMPT-READ — это значение последней ее формы, вызова READ-LINE, возвращающего прочитанную им строку (без завершающего символа новой строки).

Вы можете скомбинировать уже существующую функцию make-cd с prompt-read, чтобы построить функцию создания новой записи о CD из данных, которые make-cd по очереди получает для каждого значения. 
 
(defun prompt-for-cd ()
 (make-cd
  (prompt-read "Title")
  (prompt-read "Artist")
  (prompt-read "Rating")
  (prompt-read "Ripped [y/n]")))
 
Это почти правильно, если не считать того, что функция prompt-read возвращает строку. Это хорошо подходит для полей Title и Artist, но значения полей Rating и Ripped — числовое и булево. В зависимости от того, насколько развитым вы хотите сделать пользовательский интерфейс, можете проверять подстроки произвольной длины, чтобы удостовериться в корректности введённых пользователем данных. Теперь давайте опробуем самый очевидный (хотя и не лучший) вариант: мы можем упаковать вызов prompt-read, запрашивающий у пользователя его оценку диска, в вызов специфичной для Lisp функции PARSE-INTEGER. Это можно сделать так: 
 
(parse-integer (prompt-read "Rating"))
 
К сожалению, по умолчанию функция PARSE-INTEGER сообщает об ошибке, если ей не удаётся разобрать число из введённой строки, или если в строке присутствует «нечисловой» мусор. Однако она может принимать дополнительный параметр :junk-allowed, который позволит нам ненадолго расслабиться.
 
(parse-integer (prompt-read "Rating") :junk-allowed t)
 
Остается ещё одна проблема — если PARSE-INTEGER не удастся выделить число среди «мусорных» данных, она вернёт не число, а NIL. Следуя нашему подходу «сделать просто, пусть даже не совсем правильно», мы в этом случае можем просто задать 0 и продолжить. Макрос OR здесь — как раз то, что нужно. Это то же самое, что и операция || в Perl, Python, Java и C; макрос принимает набор выражений, поочерёдно вычисляет их и возвращает первое истинное значение (либо NIL, если все они равны NIL). Таким образом, используем следующую запись: 
 
(or (parse-integer (prompt-read "Rating") :junk-allowed t) 0)
 
 чтобы получить 0 в качестве значения по умолчанию.

Исправление кода для запроса состояния Ripped немного проще. Можно воспользоваться стандартной функцией Common Lisp Y-OR-N-P. 
 
(y-or-n-p "Ripped [y/n]: ")
 
Фактически, этот вызов является самой отказоустойчивой частью prompt-for-cd, поскольку Y-OR-N-P будет повторно запрашивать у пользователя состояние флага Ripped, если он введет что-нибудь, начинающееся не с y, Y, n или N.

Собрав код вместе, получим достаточно надёжную функцию prompt-for-cd: 
 
(defun prompt-for-cd ()
  (make-cd
   (prompt-read "Title")
   (prompt-read "Artist")
   (or (parse-integer (prompt-read "Rating") :junk-allowed t) 0)
   (y-or-n-p "Ripped [y/n]: ")))
 
Наконец, мы можем закончить интерфейс добавления CD, упаковав prompt-for-cd в функцию, циклично запрашивающую пользователя о новых данных. Воспользуемся простой формой макроса LOOP, выполняющего выражения в своём теле до тех пор, пока его выполнение не будет прервано вызовом RETURN. Например: 
 
(defun add-cds ()
  (loop (add-record (prompt-for-cd))
      (if (not (y-or-n-p "Another? [y/n]: ")) (return))))
 
Теперь с помощью add-cds добавим в базу несколько новых дисков. 
 
CL-USER> (add-cds)
Title: Rockin' the Suburbs
Artist: Ben Folds
Rating: 6
Ripped  [y/n]: y
Another?  [y/n]: y
Title: Give Us a Break
Artist: Limpopo
Rating: 10
Ripped  [y/n]: y
Another?  [y/n]: y
Title: Lyle Lovett
Artist: Lyle Lovett
Rating: 9
Ripped  [y/n]: y
Another?  [y/n]: n
NIL
 

Сохранение и загрузка базы данных

Хорошо иметь удобный способ добавления записей в базу данных. Но пользователю вряд ли понравится заново добавлять все записи после каждого перезапуска Lisp. К счастью, используя текущие структуры данных, используемые для представления информации, сохранить данные в файл и загрузить их позже — задача тривиальная. Далее приводится функция save-db, которая принимает в качестве параметра имя файла и сохраняет в него текущее состояние базы данных: 
 
(defun save-db (filename)
  (with-open-file (out filename
                   :direction :output
                   :if-exists :supersede)
    (with-standard-io-syntax
      (print *db* out))))
 
Макрос WITH-OPEN-FILE открывает файл, связывает поток с переменной, выполняет набор инструкций и затем закрывает файл. Он также гарантирует, что файл обязательно закроется, даже если во время выполнения тела макроса что-то пойдет не так. Список, находящийся сразу после WITH-OPEN-FILE, является не вызовом функции, а частью синтаксиса, определяемого этим макросом. Он содержит имя переменной, хранящей файловый поток, в который в теле макроса WITH-OPEN-FILE будет вестись запись, значение, которое должно быть именем файла, и несколько параметров, управляющих режимом открытия файла. В нашем примере файл будет открыт для записи (задаётся параметром :direction :output), и, если файл с таким именем уже существует, его содержимое будет перезаписано (параметр :if-exists :supersede).

После того, как файл открыт, всё, что вам нужно — это печать содержимого базы данных с помощью (print *db* out). В отличие от FORMAT, функция PRINT печатает объекты Lisp в форме, которую Lisp может прочитать. Макрос WITH-STANDARD-IO-SYNTAX гарантирует, что переменным, влияющим на поведение функции PRINT, присвоены стандартные значения. Используйте этот же макрос и при чтении данных из файла для гарантии совместимости опеаций записи и чтения.

Аргументом функции save-db должна являться строка, содержащая имя файла, в который пользователь хочет сохранить базу данных. Точный формат строки зависит от используемой операционной системы. Например, в Unix пользователь может вызвать функцию save-db таким образом: 
 
CL-USER> (save-db "~/my-cds.db")
((:TITLE "Lyle Lovett" :ARTIST "Lyle Lovett" :RATING 9 :RIPPED T)
 (:TITLE "Give Us a Break" :ARTIST "Limpopo" :RATING 10 :RIPPED T)
 (:TITLE "Rockin' the Suburbs" :ARTIST "Ben Folds" :RATING 6 :RIPPED T)
 (:TITLE "Home" :ARTIST "Dixie Chicks" :RATING 9 :RIPPED T)
 (:TITLE "Fly" :ARTIST "Dixie Chicks" :RATING 8 :RIPPED T)
 (:TITLE "Roses" :ARTIST "Kathy Mattea" :RATING 9 :RIPPED T))
 
В Windows имя файла может выглядеть так: c:/my-cds.db. Или так: c:\\my-cds.db.

Вы можете открыть этот файл в любом текстовом редакторе и посмотреть, как выглядят записи. Вы должны увидеть что-то очень похожее на то, что печатает REPL, если вы наберёте *db*.

Функция загрузки базы данных из файла реализуется аналогично: 
 
(defun load-db (filename)
  (with-open-file (in filename)
    (with-standard-io-syntax
      (setf *db* (read in)))))
 
 
 
 
 
 
В этот раз нет необходимости задавать :direction в параметрах WITH-OPEN-FILE, так как её значение по умолчанию — :input. И вместо печати вы используете функцию READ для чтения из потока in. Это тот же считыватель, что и в REPL, и он может прочитать любое выражение на Lisp, которое можно написать в строке приглашения REPL. Однако, в нашем случае, вы просто читаете и сохраняете выражение, не выполняя его. И снова, макрос WITH-STANDARD-IO-SYNTAX гарантирует, что READ использует тот же базовый синтаксис, что и функция save-db, когда она печатает данные с помощью PRINT.

Макрос SETF является главным оператором присваивания в Common Lisp. Он присваивает свому первому аргументу результат вычисления второго аргумента. Таким образом, в load-db переменная *db* будет содержать объект, прочитанный из файла, а именно, список списков, записанных функцией save-db. Обратите внимание на то, что load-db затирает то, что было в *db* до её вызова. Так что, если вы добавили записи, используя add-records или add-cds, и не сохранили их функцией save-db, эти записи будут потеряны. 
 
Выполнение запросов к базе данных

Теперь, когда у вас есть способ сохранения и загрузки базы данных вместе с удобным интерфейсом для добавления новых записей, ничто не мешает вам собрать такую их коллекцию, когда вы уже не захотите просто распечатывать всю базу данных для того, чтобы просмотреть её содержимое. Вам нужно как-то выполнять запросы к базе данных. Может быть, вам понравится, например, следующий способ обращения к базе: 
 
(select :artist "Dixie Chicks")
 
 Наверно, в ответ на этот запрос вы захотите получить список всех записей, в которых исполнителем является Dixie Chicks. И снова оказалось, что выбор списка в качестве контейнера данных был очень удачным.

Функция REMOVE-IF-NOT принимает предикат и список в качестве параметров и возвращает список, содержащий только элементы исходного списка, удовлетворяющие предикату. Другими словами, она удаляет все элементы, не удовлетворяющие предикату. На самом деле, REMOVE-IF-NOT ничего не удаляет — она создает новый список, оставляя исходный список нетронутым. Эта операция аналогична работе утилиты grep. Предикатом может быть любая функция, принимающая один аргумент и возвращающая логическое значение — NIL (ложь) и любое другое значение (истина).

Например, если вы хотите получить все чётные элементы из списка чисел, можете использовать REMOVE-IF-NOT таким образом: 
 
CL-USER> (remove-if-not #'evenp '(1 2 3 4 5 6 7 8 9 10))
(2 4 6 8 10)
 
В этом случае предикатом является функция EVENP, которая возвращает «истину», если её аргумент — чётное число. Нотация #' является сокращением выражения «Получить функцию с данным именем». Без #' Lisp обратится к EVENP как к имени переменной и попытается получить её значение, а не результат выполнения функции.

Вы также можете передать в REMOVE-IF-NOT анонимную функцию. Например, если бы EVENP не существовало, вы могли бы так написать предыдущее выражение: 
 
CL-USER> (remove-if-not #'(lambda (x) (= 0 (mod x 2))) '(1 2 3 4 5 6 7 8 9 10))
(2 4 6 8 10)
 
В этом случае предикатом является анонимная функция 
 
(lambda (x) (= 0 (mod x 2)))
 
которая проверяет, равен ли нулю остаток от деления аргумента на 2 (другими словами, является ли аргумент чётным). Если вы хотите извлечь только нечётные числа, используя анонимную функцию, вы можете написать следующее:
 
CL-USER> (remove-if-not #'(lambda (x) (= 1 (mod x 2))) '(1 2 3 4 5 6 7 8 9 10))
(1 3 5 7 9)
 
 Заметьте, что lambda не является именем функции — это слово показывает, что вы определяете анонимную функцию8). Если не считать имени, LAMBDA-выражение выглядит очень похожим на DEFUN: после слова lambda следует список параметров, за которым идёт тело функции.

Чтобы выбрать все альбомы Dixie Chicks из базы данных, используя REMOVE-IF-NOT, вам нужна функция, возвращающая «истину», если поле в записи artist содержит значение «Dixie Chicks». Помните, мы выбрали список свойств в качестве представления записей базы данных, потому что функция GETF может извлекать из списка свойств именованные поля. Итак, полагая, что cd является именем переменной, хранящей одну запись базы данных, вы можете использовать выражение (getf cd :artist), чтобы извлечь имя исполнителя. Функция EQUAL посимвольно сравнивает переданные ей строковые параметры. Таким образом, (equal (getf cd :artist) «Dixie Chicks») проверит, хранит ли поле artist, соответствующего данному CD, значение «Dixie Chicks». Всё, что вам нужно — упаковать это выражение в LAMBDA-форму, чтобы создать анонимную функцию и передать ее REMOVE-IF-NOT. 
 
CL-USER> (remove-if-not
  #'(lambda (cd) (equal (getf cd :artist) "Dixie Chicks")) *db*)
((:TITLE "Home" :ARTIST "Dixie Chicks" :RATING 9 :RIPPED T)
 (:TITLE "Fly" :ARTIST "Dixie Chicks" :RATING 8 :RIPPED T))
 
Предположим теперь, что вы хотите упаковать всё выражение в функцию, которая принимает имя исполнителя в качестве параметра. Вы можете записать это так:
 
(defun select-by-artist (artist)
  (remove-if-not
   #'(lambda (cd) (equal (getf cd :artist) artist))
   *db*))
 
Заметьте, что анонимная функция, содержащя код, который не будет выполнен, пока функция не вызвана в REMOVE-IF-NOT, тем не менее может ссылаться на переменную artist. В этом случае анонимная функция не просто избавляет вас от необходимости писать обычную функцию, — она позволяет вам написать функцию, наследующую часть её значений — содержимое поля artist — из контекста, в который она встроена.

Итак, мы покончили с функцией select-by-artist. Однако выборка по исполнителю — лишь одна разновидность запросов, которые вам захочется реализовать. Вы можете написать ещё несколько функций, таких, как select-by-title, select-by-rating, select-by-title-and-artist, и так далее. Но все они будут идентичными, за исключением содержимого анонимной функции. Вместо этого вы можете создать более универсальную функцию select, которая принимает функцию в качестве аргумента. 
 
(defun select (selector-fn)
  (remove-if-not selector-fn *db*))
 
А что случилось с #'? Дело в том, что в этом случае вам не нужно, чтобы функция REMOVE-IF-NOT использовала функцию под названием selector-fn. Вы хотите, чтобы она использовала анонимную функцию, переданную в качестве аргумента функции select в переменной selector-fn. Однако, символ #' вернулся в вызов select: 
 
CL-USER> (select #'(lambda (cd) (equal (getf cd :artist) "Dixie Chicks")))
((:TITLE "Home" :ARTIST "Dixie Chicks" :RATING 9 :RIPPED T)
 (:TITLE "Fly" :ARTIST "Dixie Chicks" :RATING 8 :RIPPED T))
 
Правда, это выглядит довольно грубо. К счастью, вы можете упаковать создание анонимной функции. 
 
(defun artist-selector (artist)
  #'(lambda (cd) (equal (getf cd :artist) artist)))
 
artist-selector возвращает функцию, имеющую ссылку на переменную, которая перестанет существовать после выхода из artist-selector9). Функция выглядит странно, но она работает именно так, как нам нужно — если вызвать artist-selector с аргументом «Dixie Chicks», мы получим анонимную функцию, которая ищет CD с полем :artist, содержащим «Dixie Chicks», и если вызвать её с аргументом «Lyle Lovett», то мы получим другую функцию, которая будет искать CD с полем:artist, содержащим «Lyle Lovett». Итак, мы можем переписать вызов select следующим образом: 
 
CL-USER> (select (artist-selector "Dixie Chicks"))
  ((:TITLE "Home" :ARTIST "Dixie Chicks" :RATING 9 :RIPPED T)
   (:TITLE "Fly" :ARTIST "Dixie Chicks" :RATING 8 :RIPPED T))
 
Теперь нам понадобится больше функций, чтобы генерировать выражения для выбора. Но так как вы не хотите писать select-by-title, select-by-rating и др., потому что они будут во многом схожими, вы не станете создавать множество почти идентичных генераторов выражений для выбора значений для каждого из полей. Почему бы не написать генератор функции-выражения для выбора общего назначения — функцию, которая, в зависимости от передаваемых ей аргументов, будет генерировать выражение выбора для разных полей или, может быть, даже комбинации полей? Вы можете написать такую функцию, но сначала нам придётся пройти краткий курс для овладения средством, называемым параметрами-ключами (keyword parameters).

В функциях, что вы писали до этого, вы задавали простой список параметров, которые связывались с соответствующими аргументами в вызове функции. Например, следующая функция: 
 
(defun foo (a b c) (list a b c))
 
имеет три параметра: a, b и c, и должна быть вызвана с тремя аргументами. Но иногда возникает необходимость в вызове функции, которая может вызываться с переменным числом аргументов. Параметры-ключи — один из способов это сделать. Версия foo с использованием параметров-ключей может выглядеть так: 
 
(defun foo (&key a b c) (list a b c))
 
Единственное отличие — элемент &key в начале списка аргументов. Однако вызовы новой функции foo выглядят немного по-другому. Все нижеперечисленные варианты вызова foo допустимы, результат вызова помещён справа от ==>.
 
(foo :a 1 :b 2 :c 3)  ==> (1 2 3)
(foo :c 3 :b 2 :a 1)  ==> (1 2 3)
(foo :a 1 :c 3)       ==> (1 NIL 3)
(foo)                 ==> (NIL NIL NIL)
 
Как показывают эти примеры, значения переменных a, b и c привязаны к значениям, которые следуют за соответствующими ключевыми словами. И если какой-либо ключ в вызове отсутствует, соответствующая переменная устанавливается в NIL. Я не буду уточнять, как именно задаются ключевые параметры и как они соотносятся с другими типами параметров, но вам важно знать одну деталь.
 

Обычно, когда функция вызывается без аргумента для конкретного параметра-ключа, параметр будет иметь значение NIL. Но иногда нужно различать NIL, который был явно передан в качестве аргумента к параметру-ключу, и NIL, который задаётся по умолчанию. Чтобы сделать это, при задании параметра-ключа вы можете заменить обычное имя списком, состоящим из имени параметра, его значения по умолчанию и другого имени параметра, называемого параметром supplied-p. Этот параметр supplied-p будет содержать значения «истина» или «ложь», в зависимости от того, действительно ли для данного параметра-ключа в данном вызове функции был передан аргумент. Вот версия новой функции foo, которая использует эту возможность. 
 
(defun foo (&key a (b 20) (c 30 c-p)) (list a b c c-p))
 
Результаты тех же вызовов теперь выглядят иначе: 
 
(foo :a 1 :b 2 :c 3)  ==> (1 2 3 T)
(foo :c 3 :b 2 :a 1)  ==> (1 2 3 T)
(foo :a 1 :c 3)       ==> (1 20 3 T)
(foo)                 ==> (NIL 20 30 NIL)
 
Основной генератор выражения выбора, который по причинам, которые, если вы знакомы с SQL, скоро станут очевидными, можно назвать where, является функцией, принимающей четыре параметра-ключа для соответствующих полей в наших записях CD и генерирующей выражение выбора, которое возвращает все записи о CD, совпадающие по значениями, задаваемым в where. Например, можно будет написать такое выражение: 
 
(select (where :artist "Dixie Chicks"))
 
Или такое: 
 
(select (where :rating 10 :ripped nil))
 
Функция выглядит так: 
 
(defun where (&key title artist rating (ripped nil ripped-p))
  #'(lambda (cd)
    (and
      (if title    (equal (getf cd :title)  title)  t)
      (if artist   (equal (getf cd :artist) artist) t)
      (if rating   (equal (getf cd :rating) rating) t)
      (if ripped-p (equal (getf cd :ripped) ripped) t))))
 
Эта функция возвращает анонимную функцию, возвращающую логичиеское И для одного условия в каждом поле записей о CD. Каждое условие проверяет, задан ли подходящий аргумент, и если задан, то сравнивает его значение со значением соответствующего поля в записи о CD, или возвращает t, обозначение истины в Lisp, если аргумент не был задан. Таким образом, выражение выбора возвратит t только для тех CD, описание которых совпало по значению с аргументами переданными where10). Заметьте, что, чтобы задать ключ-параметр ripped, вам необходимо использовать список из трёх элементов, потому что вам нужно знать, действительно ли вызывающая функция передала ключ-параметр :ripped nil, означающее «Выбрать те CD, в поле ripped которых установлено значение nil», либо опустила его, что означает «Мне всё равно, какое значение установлено в поле ripped». 
 
Обновление существующих записей — повторное использование where

Теперь, после того, как у вас есть достаточно универсальные функции select и where, очень логичной представляетя реализация следующей возможности, которая необходима каждой базе данных, — возможности обновления отдельных записей. В SQL команда update используется для обновления набора записей, удовлетворяющих конкретному условию where. Эта модель кажется хорошей, особенно когда у вас уже есть генератор условий where. Фактически, функция update — применение некоторых идей, которые вы уже видели: использование передаваемого выражения выбора для указания записей, подлежащих обновлению, и использование аргументов-ключей для задания нового значения. Новая вещь здесь — использование функции MAPCAR, которая отображает список, в нашем случае это *db*, и возвращает новый список, содержащий результаты вызова функции для каждого элемента исходного списка. 
 
(defun update (selector-fn &key title artist rating (ripped nil ripped-p))
  (setf *db*
        (mapcar
         #'(lambda (row)
             (when (funcall selector-fn row)
               (if title    (setf (getf row :title) title))
               (if artist   (setf (getf row :artist) artist))
               (if rating   (setf (getf row :rating) rating))
               (if ripped-p (setf (getf row :ripped) ripped)))
             row) *db*)))
 
Ещё одна новинка в этой функции — приложение SETF к сложной форме вида (getf row :title). Я расскажу о SETF подробнее в главе 6, но сейчас вам просто нужно знать, что это общий оператор присваивания, который может использоваться для присваивания друг другу различных «вещей», а не только переменных. (То, что SETF и GETF имеют настолько похожие имена — просто совпадение. Между ними нет никакой особой взаимосвязи). Сейчас достаточно знать, что после выполнения (setf (getf row :title) title) у списка свойств, на который ссылается row, значением переменной, следующей за именем свойства :title, будет title. С помощью функции update, Если вы решите, что действительно любите творчество Dixie Chicks, и что все их альбомы должны быть оценены в 11 баллов, можете выполнить следующую форму: 
 
CL-USER> (update (where :artist "Dixie Chicks") :rating 11)
NIL
 
Результат работы будет выглядеть так:
 
CL-USER> (select (where :artist "Dixie Chicks"))
((:TITLE "Home" :ARTIST "Dixie Chicks" :RATING 11 :RIPPED T)
 (:TITLE "Fly" :ARTIST "Dixie Chicks" :RATING 11 :RIPPED T))
 
Добавить функцию удаления строк из базы данных еще проще. 
 
(defun delete-rows (selector-fn)
  (setf *db* (remove-if selector-fn *db*)))
 
Функция REMOVE-IF является дополнением к REMOVE-IF-NOT; она возвращает список всех элементов, удалив те из них, что удовлетворяют предикату. Так же, как и REMOVE-IF-NOT, она, в действительности, не изменяет список, который был ей передан, но сохраняя результат обратно в *db* delete-rows действительно изменяет содержимое базы данных.
 
Избавление от дублирующего кода и большой выигрыш

До сих пор весь код базы данных, обеспечивающий операции INSERT, SELECT, UPDATE и DELETE, если не считать интерфейс командной строки для добавления новых записей и распечатки содержимого базы, укладывался в немногим более пятидесяти строк. Целиком.

Всё еще существует некоторое раздражающее дублирование кода. И, оказывается, вы можете избавиться от этого дублирования, в то же время сделав код более гибким. Дублирование, о котором я говорю, находится в функции where. Тело функции where — набор условий для каждого поля, таких, как это: 
 
(if title (equal (getf cd :title) title) t)
 
Сейчас это не так плохо, но, как и во многих случаях дублирования кода, за это всегда приходится платить одну цену: если вы хотитите изменить работу этого кода, вам нужно изменять множество копий. И если вы изменили поля в CD, вам придётся добавить или удалить условия для where. update страдает точно таким же дублированием. Это, несомненно, плохо, так как весь смысл функции where заключается в динамической генерации куска кода, проверяющего нужные нам значения; почему она должна производить работу во время выполнения, каждый раз проверяя, бело ли ей передано значение title?
 

Представьте, что вы попытались оптимизировать этот код и обнаружили, что много времени тратится на проверку того, заданы ли значения title и оставшиеся ключ-параметры. Если вы на самом деле хотите избавиться от этих проверок во время выполнения, вы можете просмотреть программу и найти все места, где вы вызываете where, и посмотреть, какие аргументы вы передаёте. Затем вы можете заменить каждый вызов where анонимной функцией, выполняющей только необходимое вычисления. Например, если вы нашли такой кусок кода: 
 
(select (where :title "Give Us a Break" :ripped t))
 
вы можете заменить его на такой:
 
(select
 #'(lambda (cd)
     (and (equal (getf cd :title) "Give Us a Break")
          (equal (getf cd :ripped) t))))
 
Заметьте, что анонимная функция отличается от той, что возвращает where; мы не пытаемся сохранить вызов where, а обеспечиваем большую производительность функции выбора. Эта анонимная функция имеет условия только для нужных нам полей, и она не производит дополнительной работы, в отличие от функции, которую может возвратить where.

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

Средство Lisp, позволяющее делать это очень просто, называется системой макросов. Подчеркиваю, что макрос в Common Lisp не имеет, в сущности, ничего общего (кроме имени) с текстовыми макросами из C и C++. В то время, как препроцессор C оперирует с текстовой подстановкой и не знает ничего о стуктуре C и C++, в Lisp макрос, в сущности, является генератором кода, который автоматически запускается для вас компилятором. Когда выражение на Lisp содержит вызов макроса, компилятор Lisp, вместо вычисления аргументов и передачи их в функцию, передает аргументы, не вычисляя их, в код макроса, который, в свою очередь, возвращает новое выражение на Lisp, которое затем вычисляется в месте исходного вызова макроса.

Я начну с простого и глупого примера и затем покажу, как вы можете заменить функцию where макросом where. Перед тем, как я напишу этот макрос-пример, мне необходимо представить вам одну новую функцию: REVERSE принимает аргумент в виде списка и возвращает новый список, который является обратным к исходному. Таким образом, (reverse '(1 2 3)) вернёт (3 2 1). Теперь попробуем создать макрос. 
 
(defmacro backwards (expr)
  (reverse expr))
 
Главное синтаксическое отличие между функцией и макросом заключается в том, что макрос определяется ключевым словом DEFMACRO, а не DEFUN. После ключевого слова в определении макроса, подобно определению функции, следует имя, список параметров и тело с выражениями. Однако макросы действуют совершенно по-другому. Вы можете использовать макрос так:
 
CL-USER> (backwards ("hello, world" t format))
hello, world
NIL
 
Как это работает? Когда REPL начинает вычислять выражение backwards, он обнаруживает, что backwards — имя макроса. Поэтому он не вычисляет выражение («hello, world» t format), что очень хорошо, так как это некорректная для Lisp структура. Далее он передаёт это список коду backwards. Код backwards передает список в функцию REVERSE, которая возвращает список (format t «hello, world»). Затем backwards передает это значение обратно REPL, который подставляет его на место исходного выражения.

Макрос backwards, таким образом, определяет новый язык, во многом похожий на Lisp — только задом наперёд — который вы можете вставлять в свой код в любой момент, просто обернув обратное выражение на Lisp в вызов макроса backwards. И в скомпилированной программе на Lisp этот новый язык покажет такую же производительность, как и обычный Lisp, потому что весь код в макросе — код, сгенерированный в новом выражении — выполняется во время компиляции. Другими словами, компилятор сгенерирует один и тот же код код, независимо от того, напишете вы (backwards («hello, world» t format)) или (format t «hello, world»).

Итак, как это поможет решить проблему дублирующегося кодом в where? Очень просто. Вы можете написать макрос, генерирующий совершенно такой же код, какой вы написали бы для каждого вызова where. И снова, лучший подход — это разрабатывать код снизу вверх. В оптимизированной вручную функции выбора where для каждого из заданных полей у вас было выражение в следующей форме: 
 
(equal (getf cd field) value)
 
Давайте напишем функцию, которая, получив имя поля и некоторое значение, возвращает такое выражение. Так как выражение — это просто список, можно написать что-нибудь вроде:
 
(defun make-comparison-expr (field value)    ; неправильно
(list equal (list getf cd field) value))
 
Однако здесь имеется небольшой нюанс: как вы знаете, когда Lisp обнаруживает просто имя вроде field или value, а не первый элемент списка, он полагает, что это имя переменной, и пытается получить ее значение. Это нормально для field и value; это именно то, что нужно. Но он будет обращаться к equal, getf и cd таким же образом, а это в нашем случае нежелательно. Вы, однако, знаете также, как не позволить Lisp пытаться вычислить структуру: поместить перед ней одиночную кавычку ('). Таким образом, если вы напишете функцию make-comparison-expr вот так, она сделает то, что вам нужно: 
 
(defun make-comparison-expr (field value)
  (list 'equal (list 'getf 'cd field) value))
 

Вы можете проверить её работу в REPL: 
 
CL-USER> (make-comparison-expr :rating 10)
(EQUAL (GETF CD :RATING) 10)
CL-USER> (make-comparison-expr :title "Give Us a Break")
(EQUAL (GETF CD :TITLE) "Give Us a Break")
 
Но, оказывается, существует лучший способ сделать это. То, что вам действительно нужно, — это иметь возможность написать выражение, которое в большинстве случаев не вычисляется, и затем каким-либо образом выбирать некоторые выражения, которые вы хотите вычислить. И, конечно же, такой механизм существует. Обратная кавычка (`) перед выражением не позволяет вычислить выражение точно так же, как и прямая одиночная кавычка. 
 
CL-USER> `(1 2 3)
(1 2 3)
CL-USER> '(1 2 3)
(1 2 3)
 
Однако в выражении с обратной кавычкой любое подвыражение, перед которым стоит запятая, вычисляется. Обратите внимание на влияние запятой во втором выражении:
 
`(1 2 (+ 1 2))  ==> (1 2 (+ 1 2))
`(1 2 ,(+ 1 2)) ==> (1 2 3)
 
Используя обратную кавычку, вы можете переписать функцию make-comparison-expr следующим образом: 
 
(defun make-comparison-expr (field value)
  `(equal (getf cd ,field) ,value))
 
Теперь, если вы посмотрите на оптимизированную вручную функцию выбора, вы увидите, что тело функции состоит из одного оператора сравнения для каждой пары поле/значение, обернутое в выражение AND. На мгновение положим, что вам нужно расположить аргументы таким образом, чтобы передать их макросу where единым списком. Вам понадобится функция, которая принимает аргументы этого списка попарно и сохраняет результаты выполнения вызова make-comparison-expr для каждой пары. Чтобы реализовать эту функцию, вы можете воспользоваться мощным макросом LOOP. 
 
(defun make-comparisons-list (fields)
  (loop while fields
     collecting (make-comparison-expr (pop fields) (pop fields))))
 
Полное описание макроса LOOP отложим до 22 главы, а сейчас заметим, что выражение LOOP выполняет именно то, что требуется: оно циклично проходит по всем элементам в списке fields, каждый раз возвращая по два элемента, передавет их в make-comparison-expr и сохраняет возвращаемые результаты, чтобы их вернуть при выходе из цикла. Макрос POP выполняет операцию, обратную операции, выполняемой макросом PUSH, который вы использовали для добавления записей в *db*.

Теперь вам нужно просто обернуть список, возвращаемый функцией make-comparison-list в AND и анонимную функцию, которую вы можете реализовать прямо в макросе where. Это просто: используйте обратную кавычку, чтобы создать шаблон, который будет заполнен значениями функции make-comparison-list. 
 
(defmacro where (&rest clauses)
  `#'(lambda (cd) (and ,@(make-comparisons-list clauses))))
 
Этот макрос использует вариацию , (а именно, ,@) перед вызовом make-comparison-list. Сочетание ,@ «вклеивает» значение следующего за ним выражения, которое должно возвращать список, во «внешний» список. 
 
`(and ,(list 1 2 3))  ==> (AND (1 2 3))
`(and ,@(list 1 2 3)) ==> (AND 1 2 3)
 
Вы также можете использовать ,@ для «вклейки» элементов в середину списка: 
 
`(and ,@(list 1 2 3) 4) ==> (AND 1 2 3 4)
 
Другая важная особенность макроса where — использование &rest в списке аргументов. Так же, как и &key, &rest изменяет способ разбора аргументов. Если в списке параметров обнаруживается &rest, функция или макрос могут принимать произвольное число аргументов, которые собираются в единый список, становящийся значением переменной, имя которой следует за &rest. Итак, если вы вызовите where так: 
 
(where :title "Give Us a Break" :ripped t)
 
переменная clauses будет содержать список: 
 
(:title "Give Us a Break" :ripped t)
 
Этот список передается фунции make-comparisons-list, которая возвращает список выражений сравнения. С помощью функции MACROEXPAND-1 вы можете точно видеть, какой код будет сгенерирован where. Если вы передадите в MACROEXPAND-1 форму, являющуюся вызовом макроса, она вызовет макрос с заданными аргументами и вернёт его развёрнутый вид. Итак, вы можете проверить предыдущий вызов where следующим образом: 
 
CL-USER> (macroexpand-1 '(where :title "Give Us a Break" :ripped t))
#'(LAMBDA (CD)
    (AND (EQUAL (GETF CD :TITLE) "Give Us a Break")
         (EQUAL (GETF CD :RIPPED) T)))
T
 
Выглядит неплохо. Теперь попробуем испытать макрос в действии: 
 
CL-USER> (select (where :title "Give Us a Break" :ripped t))
((:TITLE "Give Us a Break" :ARTIST "Limpopo" :RATING 10 :RIPPED T))
 
Работает. И макрос where с его двумя функциями-помощниками оказался на одну строку короче, чем старая функция where. И, что самое главное, where больше не привязана к конкретным полям наших записей о CD. 
 
Об упаковке

Случилась интересная вещь. Вы избавились от дублирования и сделали код одновременно более производительным и универсальным. Так часто бывает, если правильно выбрать макрос. Это имеет смысл, потому что макрос — это ещё один механизм создания абстракций — абстракций на синтаксическом уровне, а абстракции — это, по определению, более короткий путь для выражения подразумеваемых сущностей. Сейчас код мини-базы данных, который относится к CD и полям, его описывающим, находится только в функциях make-cd, prompt-for-cd и add-cd. Фактически, наш новый макрос будет работать с любой базой данных, основанной на списке свойств.

Тем не менее, эта база данных всё еще далека от завершения. Вероятно, вы думаете о добавлении множества возможностей, например, таких, как поддержка множества таблиц или более сложных запросов. В главе 27 мы создадим базу данных о записях MP3, которая будет содержать некоторые из этих возможностей.


Синтаксис и семантика

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

Синтаксис Lisp немного отличается от синтаксиса языков, произошедших от Algol. Две наиболее очевидные черты - это обширное использование скобок и префиксная нотация. По этим причинам многие люди пугаются этого синтаксиса. Очернители Lisp склонны описывать его синтаксис как «непонятный» и «раздражающий». Название «Lisp», по их словам, должно обозначать «Множество Раздражающих Ненужных Скобок» (Lots of Irritating Superfluous Parentheses). С другой стороны, люди, использующие Lisp, склонны рассматривать синтаксис Lisp как одно из главных его достоинств. Как может быть то, что так не нравится одной группе, быть предметом восхищения другой?

Я не могу действительно объяснить вам все состояние дел с синтаксисом Lisp, пока я не расскажу о макросах Lisp немного подробней, но я могу начать с предыстории, которая наводит на мысль, что это стоит иметь ввиду: когда John McCarthy изобрел Lisp, он намеревался реализовать его в более Algol-подобном синтаксисе, который он называл M-выражения. Однако он так не и сделал этого.
 
Проект по точному определению М-выражений и их компиляции или, хотя бы, трансляции их в S-выражения не был ни завершен, ни явно заброшен. Он просто был отложен на неопределенное будущее, а тем временем появилось новое поколение программистов, которые предпочитали S-выражения любой Fortran- или Algol-подобной нотации, которая только может быть выдумана.

Другими словами, люди, которые действительно использовали Lisp на протяжении последних 45 лет, полюбили синтаксис и нашли, что он делает язык более мощным. По прочтении последующих глав вы начнете понимать почему.
 
Разделение черного ящика

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

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

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

В Common Lisp фазы разбиты иначе, с последствиями как для конструкторов реализации, так и для определения языка. Вместо одного черного ящика, который осуществляет переход от текста программы к ее поведению за один шаг, Common Lisp определяет два черных ящика, первый из которых транслирует текст в объекты Lisp, а другой реализует семантику языка в терминах этих объектов. Первый ящик называется процедурой чтения, а второй - процедурой вычисления.

Каждый черный ящик определяет один уровень синтаксиса. Процедура чтения определяет как строки знаков могут транслироваться в объекты, называемые s-выражениями. Так как синтаксис s-выражений включает синтаксис для списков произвольных объектов, включая другие списки, s-выражения могут представлять произвольные древовидные выражения (tree expressions), очень похожие на абстрактные синтаксические деревья, генерируемые синтаксическими анализаторами не-Lisp языков.

В свою очередь, процедура вычисления определяет синтаксис форм Lisp, которые могут быть построены из s-выражений. Не все s-выражения являются допустимыми формами Lisp также как и не все последовательности знаков являются допустимыми s-выражениями. Например, и (foo 1 2), и («foo» 1 2) являются s-выражениями, но только первое может быть формой Lisp, так как список, который начинается со строки не является формой Lisp.

Это разделение черного ящика имеет несколько следствий. Одно их них состоит в том, что вы можете использовать s-выражения, как вы видели в качестве внешнего формата для данных, не являющихся исходным кодом, используя READ для их чтения и PRINT для их записи. Другое следствие состоит в том, что так как семантика языка определена в терминах деревьев объектов, а не в терминах строк знаков, то генерировать код внутри языка становится легче, чем это можно было бы сделать, если бы код генерировался как текст. Генерирование кода полностью с нуля не намного легче: и построение списков, и построения строк являются примерно одинаковыми по сложности работами. Однако реальный выигрыш в том, что вы можете генерировать код, манипулируя существующими данными. Это является базой для макросов Lisp, которые я опишу гораздо подробнее в будущих главах. Сейчас я сфокусируюсь на двух уровнях синтаксиса, определенных Common Lisp: синтаксис s-выражений, понимаемых процедурой чтения, и синтаксис форм Lisp, понимаемый процедурой вычисления.
 
S-выражения

Базовыми элементами s-выражения являются списки и атомы. Списки ограничиваются скобками и могут содержать любое число разделенных пробелами элементов. Все, что не список, является атомом5). Элементами списков в свою очередь также являются s-выражения (другими словами, атомы или вложенные списки). Комментарии (которые, строго говоря, не являются s-выражениями) начинаются с точки с запятой, распространяются до конца строки, и трактуются как пробел.

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

С числами все довольно очевидно: любая последовательность цифр (возможно, начинающаяся со знака (+ или -), содержащая десятичную точку или знак деления, и, возможно, заканчивающаяся меткой показателя степени) трактуется как число. Например: 
 
123       ; целое число "сто двадцать три"
3/7       ; отношение "три седьмых"
1.0       ; число с плавающей точкой "один" с точностью, заданной по умолчанию
1.0e0     ; другой способ записать то же самое число с плавающей точкой
1.0d0     ; число с плавающей точкой "один" двойной точности
1.0e-4    ; эквивалент с плавающей точкой числа "одна десятитысячная"
+42       ; целое число "сорок два"
-42       ; целое отрицательное число "минус сорок два"
-1/4      ; отношение "минус одна четвертая"
-2/8      ; другой способ записать то же отношение
246/2     ; другой способ записать целое "сто двадцать три"
 
 Эти различные формы представляют различные типы чисел: целые, рациональные, числа с плавающей точкой. Lisp также поддерживает комплексные числа, которые имеют свою собственную нотацию и которые мы рассмотрим в главе 10.

Как показывают некоторые из этих примеров, вы можете задать одно и то же число множеством различных способов. Но независимо от того, как вы запишите их, все рациональные (целые и отношения) внутри Lisp представляются в «упрощенной» форме. Другими словами, объекты, которые представляют числа -2/8 и 246/2, не отличаются от объектов, которые представляют числа -1/4 и 123. Таким же образом, 1.0 и 1.0e0 – просто два разных способа записать одно число. С другой стороны, 1.0, 1.0d0 и 1 могут представлять различные объекты, так как различные представления чисел с плавающей точкой и целых чисел являются различными типами. Мы рассмотрим детали характеристик различных типов чисел в главе 10.

Строковые литералы, как вы видели в предыдущей главе, заключаются в двойные кавычки. Внутри строки обратный слеш (\) экранирует следующий знак, что вызывает включение этого знака в строку «как есть». Только два знака должны быть экранированы в строке: двойная кавычка и сам обратный слеш. Все остальные знаки могут быть включены в строковый литерал без экранирования, не обращая внимания на их значение вне строки. Несколько примеров строковых литералов: 
 
"foo"     ; строка, содержащая знаки 'f', 'o' и 'o'.
"fo\o"    ; такая же строка.
"fo\\o"   ; строка, содержащая знаки 'f', 'o', '\' и 'o'.
"fo\"o"   ; строка, содержащая знаки 'f', 'o', '"' и 'o'.
 
Имена, использующиеся в программах на Lisp, такие как FORMAT, hello-world и *db* представляются объектами, называющимися символами. Процедура чтения ничего не знает о том, как данное имя будет использоваться – является ли оно именем переменной, функции или чем-то еще. Она просто читает последовательность знаков и создает объект, представляющий имя. Почти любой знак может входить в имя. Однако, это не может быть пробельный символ, так как пробелом разделяются элементы списка. Цифры могут входить в имена, если имя целиком не сможет интерпретироваться как число. Схожим образом, имена могут содержать точки, но процедура чтения не может прочитать имя, состоящее только из точек. Существует десять знаков, которые не могут входить в имена, так как предназначены для других синтаксических целей: открывающая и закрывающая скобки, двойные и одинарные кавычки, обратный апостроф, запятая, двоеточие, точка с запятой, обратный слеш и вертикальная черта. Но даже эти знаки могут входить в имена, если их экранировать обратным слешем или окружить часть имени, содержащую знаки, которые нужно экранировать, с помощью вертикальных линий.

Две важные характерные черты того, каким образом процедура чтения переводит имена в символьные объекты, касаются того, как она трактует регистр букв в именах и как она обеспечивает то, чтобы одинаковые имена всегда читались как одинаковые символы. Во время чтения имен процедура чтения конвертирует все неэкранированные знаки в именах в их эквивалент в верхнем регистре. Таким образом, процедура чтения прочитает foo, Foo и FOO как одинаковый символ: FOO. Однако, \f\o\o и |foo| оба будут прочитаны как foo, что будет отличным от символа FOO объектом. Это как раз и является причиной, почему при определении функции в REPL, он печатает имя функции, преобразованное к верхнему регистру. Сейчас стандартным стилем является написание кода в нижнем регистре, позволяя процедуре чтения преобразовывать имена к верхнему.

Чтобы быть уверенным в том, что одно и то же текстовое имя всегда читается как один и тот же символ, процедура чтения хранит все символы – после того, как она прочитала имя и преобразовала его к верхнему регистру, процедура чтения ищет в таблице, называемой пакетом (package), символ с таким же именем. Если она не может найти такой, то она создает новый символ и добавляет его к таблице. Иначе она возвращает символ, уже хранящийся в таблице. Таким образом, где бы одно и то же имя не появлялось в любых s-выражениях, оно будет представлено одним и тем же объектом.

Так как имена в Lisp могут содержать намного большее множество знаков, чем языках, произошедших от Algol, в Lisp существуют определенные соглашения по именованию, такие как использование дефисов в именах наподобие hello-world. Другое важное соглашение состоит в том, что глобальным переменным дают имена, начинающиеся и заканчивающиеся знаком *. Подобным образом, константам дают имена, начинающиеся и заканчивающиеся знаком +. Также некоторые программисты называют очень низкоуровневые функции именами, начинающимися с % или даже %%. Имена, определенные в стандарте языка, используют только алфавитные знаки (A-Z), а также *, +, -, /, 1, 2, <, =, >, &.

Синтаксис для списков, чисел, строк и символов описывает большую часть Lisp программ. Другие правила описывают нотацию для векторных литералов, отдельных знаков, массивов, которые я опишу в главах 10 и 11, когда мы будем говорить об этих типах данных. Сейчас главным является понимание того, как комбинируются числа, строки и символы с разделенными скобками списками для построения s-выражений, представляющих произвольные деревья объектов. Несколько простых примеров: 
 
x             ; символ X
()            ; пустой список
(1 2 3)       ; список из трех элементов
("foo" "bar") ; список из двух строк
(x y z)       ; список из трех символов
(x 1 "foo")   ; список из символа, числа и строки
(+ (* 2 3) 4) ; список из символа, списка и числа
 
Еще одним чуть более сложным примером является четырехэлементный список, содержащий два символа, пустой список и другой список, в свою очередь содержащий два символа и строку: 
 
(defun hello-world ()
  (format t "hello, world"))
 

S-выражения как формы Lisp

После того, как процедура чтения преобразовывает текст в s-выражения, эти s-выражения могут быть вычислены как код Lisp. Точнее некоторые из них могут – не каждое s-выражение, которое процедура чтения может прочитать, обязательно может быть вычислено как код Lisp. Правила вычислений Common Lisp определяют второй уровень синтаксиса, который определяет, какие s-выражения могут трактоваться как формы Lisp. Синтаксические правила на этом уровне очень просты. Любой атом (не список или пустой список) является допустимой формой Lisp, а также любой список, который содержит символ в качестве своего первого элемента, также является допустимой формой Lisp.

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

Простейшие формы Lisp, атомы, могут быть разделены на две категории: символы и все остальное. Символ, вычисляемый как форма, трактуется как имя переменной и вычисляется в ее текущее значение. Я обсужу в главе 6 как переменные получают свои значения впервые. Также следует заметить, что некоторые «переменные» являются старым программистским оксюмороном: «константными переменными». Например, символ PI именует константную переменную, чье значение – число с плавающей точкой, являющееся наиболее близкой аппроксимацией математической константы pi.

Все остальные атомы (числа и строки) являются типом объектов, который вы уже рассмотрели – это самовычисляемые объекты (self-evaluating objects). Это означает, что когда выражение передается в воображаемую функцию вычисления, оно просто возвращается. Вы видели примеры самовычисляемости объектов в главе 2, когда набирали 10 и «hello, world» в REPL.

Символы также могут быть самовычисляемыми в том смысле, что переменной, которую именует такой символ, может быть присвоено значение самого этого символа. Две важные константы определены таким образом: T и NIL, стандартные истинное и ложное значения. Я обсужу их роль как логических выражений в секции «Правда, ложь и равенство».

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

Все становится гораздо интереснее при рассмотрении того, как вычисляются списки. Все допустимые формы списков начинаются с символа, но существуют три разновидности форм списков, которые вычисляются тремя различными способами. Для определения того, какую разновидность формы представляет из себя данный список, процедура вычисления должна определить чем является первый символ списка: именем функции, макросом или специальным оператором. Если символ еще не был определен (такое может быть в случае, если вы компилируете код, который содержит ссылки на функции, которые будут определены позднее) – предполагается, что он является именем функции. Я буду ссылаться на эти три разновидности форм как на формы вызова функции (function call forms), формы макроса (macro forms) и специальные формы (special forms).
 
Вызовы функций

Правило вычисления для форм вызова функции просто: вычисление элементов списка, начиная со второго, как форм Lisp и передача результатов в функцию, именованную первым элементом. Это правило явно добавляет несколько дополнительных синтаксических ограничений на форму вызова функции: все элементы списка после первого должны также быть правильными формами Lisp. Другими словами, базовый синтаксис формы вызова функции следующий (каждый аргумент также является формой Lisp): 
 
(function-name argument*)
 
Таким образом, следующее выражение вычисляется путем первоначального вычисления 1, затем 2, а затем передачи результатов вычислений в функцию +, которая возвращает 3: 
 
(+ 1 2)
 
Более сложное выражение, такое как следующее, вычисляется схожим образом за исключением того, что вычисление аргументов (+ 1 2) и (- 3 4) влечет за собой вычисление аргументов этих форм и применение соответствующих функций к ним: 
 
(* (+ 1 2) (- 3 4))
 
В итоге, значения 3 и -1 передаются в функцию *, которая возвращает -3.

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

Нужно сказать, что не все операции могут быть определены как функции. Так как все аргументы функции вычисляются перед ее вызовом, не существует возможности написать функцию, которая ведет себя как оператор IF, который вы использовали в главе 3. Для того, чтобы увидеть почему, рассмотрим такую форму: 
 
(if x (format t "yes") (format t "no"))
 
Если IF является функцией, процедура вычисления будет вычислять аргументы выражения слева направо. Символ x будет вычислен как переменная, возвращающая свое значение; затем как вызов функции будет вычислена (format t «yes»), возвращающая NIL после печати «yes» на стандартный вывод; и затем будет вычислена (format t «no»), печатающая «no» и возвращающая NIL. Только после того, как эти три выражения будут вычислены, их результаты будут переданы в IF, слишком поздно для того, чтобы проконтролировать, какое из двух выражений FORMAT будет вычислено.

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

Если первый элемент списка является символом, именующим специальный оператор, остальная часть выражения вычисляется в соответствии с правилом для этого оператора.

Правило для IF очень просто: вычисление первого выражения. Если оно вычисляется не в NIL, то вычисляется следующее выражение и возвращается его результат. Иначе возвращается значение вычисления третьего выражения или NIL, если третье выражение не задано. Другими словами, базовая форма выражения IF следующая: 
 
(if test-form then-form [ else-form ])
 
test-form вычисляется всегда, а затем только одна из then-form и else-form.

Еще более простой специальный оператор - это QUOTE, который получает одно выражение как аргумент и просто возвращает его не вычисляя. Например, следующая форма вычисляется в список (+ 1 2), а не значение 3: 
 
(quote (+ 1 2))
 
Этот список не отличается ни от какого другого, вы можете манипулировать им также, как и любым другим, который вы можете создать с помощью функции LIST.

QUOTE используется достаточно часто, поэтому для него в процедуру чтения был встроен специальный синтаксис. Вместо написания такого: 
 
(quote (+ 1 2))
 
вы можете написать это: 
 
'(+ 1 2)
 
Этот синтаксис является небольшим расширением синтаксиса s-выражений, понимаемый процедурой чтения. С этой точки зрения для процедуры вычисления оба этих выражения выглядят одинаково: список, чей первый элемент является символом QUOTE, а второй элемент – список (+ 1 2)15).

В общем, специальные операторы реализуют возможности языка, которые требуют специальной обработки процедурой вычисления. Например, некоторые специальные операторы манипулируют окружением, в котором вычисляются другие формы. Один из них, который я обсужу детально в главе 6, - LET, который используется для создания новой привязки переменной (variable binding). Следующая форма вычисляется в 10, так как второй x вычисляется в окружении, где он именует переменную, связанную оператором LET со значением 10: 
 
(let ((x 10)) x)
 

Макросы

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

Очень важно понимать обе фазы вычисления форм макросов. Очень легко запутаться когда вы печатаете выражения в REPL, так как эти две фазы происходят одна за одной и значение второй фазы немедленно возвращается. Но, когда код Lisp компилируется, эти два фразы выполняются в разное время, и очень важно понимать, что и когда происходит. Например, когда вы компилируете весь файл с исходным кодом с помощью функции COMPILE-FILE, все формы макросов в файле рекурсивно раскрываются, пока код не станет содержать ничего кроме форм вызова функций и специальных форм. Этот не содержащий макросов код затем компилируется в файл FASL, который функция LOAD знает как загрузить. Скомпилированный код, однако, не выполняется пока файл не будет загружен. Так как макросы генерируют свое расширение во время компиляции, они могут проделывать довольно большой объем работы, генерируя свои раскрытия, без платы за это во время загрузки файла или при вызове функций, определенных в этом файле.

Так как процедура вычисления не вычисляет элементы формы макроса перед передачей их в функцию макроса, они не обязательно должны быть правильными формами Lisp. Каждый макрос назначает смысл s-выражениям, используемым в форме этого макроса (macro form), посредством того, как он использует эти s-выражения для генерации своего расширения. Другими словами, каждый макрос определяет свой собственный локальный синтаксис. Например, макрос переворачивания списка задом наперед из главы 3 определяет синтаксис, в котором выражение является допустимой перевернутой формой если ее список, будучи перевернутым, является допустимой формой Lisp.

Я расскажу больше о макросах в этой книге. А сейчас вам важно понимать, что макросы, несмотря на то, что синтаксически похожи на вызовы функции, служат иной цели, предоставляя добавочный уровень к компилятору.
 
Истина, Ложь и Равенство

Оставшейся частью базовых знаний, которые вам необходимо получить, являются понятия истины, лжи и равенства объектов в Common Lisp. Понятия истины и лжи очень просты: символ NIL является единственным ложным значением, а все остальное является истиной. Символ T является каноническим истинным значением и может быть использован когда вам нужно вернуть не-NIL значение, но само значение не важно. Единственной хитростью является то, что NIL также является единственным объектом, который одновременно является и атомом и списком: вдобавок к представлению ложного значения он также используется для представления пустого списка. Эта равнозначность NIL и пустого списка встроена в процедуру чтения: если процедура чтения видит (), она считывает это как символ NIL. Обе записи полностью взаимозаменяемые. И так как NIL, как я уже упоминал раньше, является именем константной переменной, значением которой является символ NIL, то выражения nil, (), 'nil и '() вычисляются в одинаковый объект: unquoted формы вычисляются как ссылки на константную переменную, чье значения – символ NIL, а quoted формы специальный оператор QUOTE вычисляет в символ напрямую. По этим же причинам, и t и 't будут вычислены в одинаковый объект: символ T.

Использование фраз, таких как «то же самое», конечно рождает вопрос о том, что для двух значений значит «то же самое». Как вы увидите в следующих главах, Common Lisp предоставляет ряд типо-зависимых предикатов равенства: = используется для сравнения чисел; CHAR= для сравнения знаков и т.д. В этой секции мы рассмотрим четыре «общих» («generic») предиката равенства – функции, которым могут быть переданы два Lisp-объекта, и которые возвратят истину, если эти объекты эквивалентны, и ложь в противном случае. Вот они в порядке ослабления понятия «различности»: EQ, EQL, EQUAL, и EQUALP.

EQ проверяет «идентичность объектов»: она возвращает истинное значение если два объекта идентичны. К сожалению, понятие идентичности таких объектов, как числа и знаки, зависит от того, как эти типы данных реализованы в конкретной реализации Lisp. Таким образом, EQ может считать два числа или два знака с одинаковым значением, как эквивалентными, так и нет. Стандарт языка оставляет реализациям достаточную свободу действий в этом вопросе, что приводит к тому, что выражение (eq 3 3) может вполне законно вычисляться как в истинное, так и в ложное значение. Таким же образом (eq x x) может вычисляться как в истинное, так и в ложное значение в различных реализациях если значением x является число или знак.

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

Поэтому, Common Lisp определяет EQL, работающую аналогично EQ, за исключением того, что она также гарантирует рассмотрение эквивалентными двух объектов одного класса, представляющих одинаковое числовое или знаковое (character) значение. Поэтому (eql 1 1) гарантировано будет истиной. А (eql 1 1.0) гарантировано будет ложью, так как целое значение 1 и значение с плавающей точкой 1.0 являются представителями различных классов.

Существуют два лагеря по отношению к вопросу где использовать EQ и где использовать EQL: сторонники «когда возможно всегда используйте EQ» убеждают вас использовать EQ, когда вы уверены, что не будете сравнивать числа или знаки так как: a) это способ указать, что вы не собираетесь сравнивать числа и символы; b) это будет немного эффективней, так как EQ не нужно проверять, являются ли ее аргументы числами или знаками.

Сторонники «всегда используйте EQL» советуют вам никогда не использовать EQ, так как (а) потенциальный выигрыш в ясности теряется, так как каждый раз, когда кто-либо будет читать ваш код (включая вас) и увидит EQ, он должен будет остановиться и проверить, корректно ли эта функция используется (другими словами, проверить, что она никогда не вызывается для сравнения цифр или знаков) и (b) различие в эффективности между EQ и EQL очень мало по сравнению с производительностью в действительно узких местах.

Другие два предиката равенства EQUAL и EQUALP являются общими в том смысле, что они могут оперировать всеми типами объектов, но они не настолько фундаментальные, как EQ или EQL. Каждый из них определяет несколько более слабое понятие «различности», чем EQL, позволяя другим объектам считаться эквивалентным. Нет ничего особенного в тех конкретных понятиях эквивалентности, что реализуют эти функции, за исключением того, что они оказались полезными Lisp-программистам прошлого. Если эти предикаты не подходят вам, вы всегда можете определить свой собственный предикат для сравнения объектов других типов нужным вам способом.

EQUAL ослабляет понятие «различности» между EQL, считая списки эквивалентными, если они рекурсивно, согласно тому же EQUAL, имеют одинаковую структуру и содержимое. EQUAL также считает строки эквивалентными, если они содержат одинаковые символы. EQUAL также ослабляет понятие «различности» по сравнению с EQL для битовых векторов (bit vectors) и путей – двух типах, о которых я расскажу в следующих главах. Для всех остальных типов он аналогичен EQL.

EQUALP аналогична EQUAL за исключением еще большего ослабления понятия «различности». EQUALP считает две строки эквивалентными, если они имеют одинаковые символы, игнорируя разницу в регистре. Два символа также считаются эквивалентными, если они отличается только регистром. Числа эквивалентны по EQUALP, если они представляют одинаковое математическое значение. Например, (equalp 1 1.0) вернет истину. Списки, элементы которых попарно эквивалентны по EQUALP, считаются эквивалентными; подобным же образом массивы с элементами, эквивалентными по EQUALP, также считаются эквивалентными. Как и в случае с EQUAL, существует несколько других типов данных, которые я пока не рассмотрел, для которых EQUALP может рассмотреть два объекта эквивалентными, в то время как EQL и EQUAL будут считать их различными. Для всех остальных типов данных EQUALP аналогична EQL.
 

Форматирование кода Lisp

Хотя форматирование кода, строго говоря, не имеет ни синтаксического, ни семантического значения, хорошее форматирование важно для легкого чтения и написания кода. Ключевым моментов в форматировании кода Lisp является правильная расстановка отступов. Отступы должны отражать структуру кода так, чтобы вам не пришлось считать скобки для его понимания. Вообще, каждый новый уровень вложенности должен иметь больший отступ, а если нужен перенос строки, то элементы следующей строки имеют тот же уровень вложенности, что и предыдущей. Таким образом, вызов функции, который должен быть разбит на несколько строк может быть записан следующим образом: 
 
  (some-function arg-with-a-long-name
                 another-arg-with-an-even-longer-name)
 
Расстановка отступов в макросах и специальных формах, которые реализуют структуры контроля, обычно немного отличается: элементы «тела» отступаются на два пробела относительно открывающей скобки формы. Таким образом: 
 
  (defun print-list (list)
    (dolist (i list)
      (format t "item: ~a~%" i)))
 
Однако, вам не нужно сильно беспокоиться на счет этих правил, так как хорошая среда Lisp, такая как SLIME, возьмет эту заботу на себя. Фактически, одним из преимуществ регулярного синтаксиса Lisp является то, что программному обеспечению, такому как текстовые редакторы, очень легко расставлять отступы. Так как расстановка отступов нужна для отражения структуры кода, а структура определяется скобками, легко позволить редактору расставить отступы вместо вас.

В SLIME нажатие Tab в начале каждой строки приводит к тому, что строка будет правильно выравнена; также вы можете перевыровнять целое выражение, поставив курсор на открывающую скобку и набрав C-M-q. Или вы можете перевыровнять все тело функции, набрав C-c M-q, находясь где угодно в теле функции.

На самом деле, опытный Lisp-программист предпочитает полагаться на текстовый редактор, который обработает отступы автоматически, не только для того, чтобы код выглядел красиво, но и для обнаружения опечаток: как только вы привыкните к тому, как отступы в коде расставляются правильно, потерянная скобка будет легко распознаваться по странной расстановке отступов вашим редактором. Например, предположим, что вы написали следующую функцию:
 
  (defun foo ()
    (if (test)
      (do-one-thing)
      (do-another-thing)))
 
Теперь предположим, что вы случайно не закрыли левую скобку после test. Поскольку вы не обеспокоены подсчетом скобок, вы просто добавите дополнительную скобку в конец формы DEFUN, получив следующий код: 
 
  (defun foo ()
    (if (test
      (do-one-thing)
      (do-another-thing))))
 
Однако, если вы выравнивали код, нажимая Tab в начале каждой строки, вы не получите вышеприведенный код. Вместо него вы получите это: 
 
  (defun foo ()
    (if (test
         (do-one-thing)
         (do-another-thing))))
 
Выравнивание веток if и then, перенесенных под условие вместо того, чтобы находиться чуть правее if, немедленно говорит нам, что что-то не так.

Другое важное правило форматирования заключается в том, что закрывающие скобки всегда помещаются в той же строке, что и последний элемент списка, который они закрывают. Так что не пишите так: 
 
  (defun foo ()
    (dotimes (i 10)
      (format t "~d. hello~%" i)
    )
  )
 
правильный вариант: 
 
  (defun foo ()
    (dotimes (i 10)
      (format t "~d. hello~%" i)))
 
Строка ))) в конце может казаться некрасивой, но когда ваш код имеет правильные отступы скобки должны уходить на второй план. Не нужно привлекать к ним несвоевременное внимание, располагая их на нескольких строках.

И, наконец, комментарии должны предваряться от одной до четырех точек с запятой, в зависимости от контекста появления этого комментария:
 
  ;;;; Четыре точки с запятой для комментария в начале файла
 
  ;;; Комментарий из трех точек с запятой обычно является параграфом комментариев,
  ;;; который предваряет большую секцию кода
 
  (defun foo (x)
    (dotimes (i x)
      ;; Две точки с запятой показывают, что комментарий применен к последующему коду.
      ;; Заметьте, что этот комментарий имеет такой же отступ, как и последующий код.
      (some-function-call)
      (another i)              ; этот комментарий применим только к этой строке
      (and-another)            ; а этот для этой строки
      (baz)))
 
Теперь вы готовы начать более детально рассматривать важнейшие строительный блоки программ Lisp: функции, переменные и макросы. Следующим шагом станут функции.
 
 
Функции

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