detective Peter Seibel Practical Common Lisp Апрель 2005 pcl.lisp 2010-03-15 1.0

2010-03-15 Исходная версия

1
<p>1. Введение: почему Lisp?</p>

Если вы считаете, что величайшее удовольствие в программировании доставляет код, делающий многое и выражающий ваши желания просто и ясно, тогда программирование на 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-файлах.

<p>Почему Lisp?</p>

Сложно объяснить на нескольких страницах введения, почему пользователи языка любят какой-то конкретный язык, ещё сложнее объяснить, почему вы должны тратить своё время на его изучение. Личный пример не слишком убеждает. Может быть, я люблю 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 стараются предоставить наиболее прозрачное отображение между вашими идеями о том, как программа должна работать, и кодом, который вы пишете. Ваши идеи не замутняются нагромождением кода и бесконечно повторяющимися выражениями. Это делает ваш код более управляемым, потому что вам больше не приходится бродить по нему всякий раз, когда вы хотите внести какие-то изменения. Даже систематические изменения в программе могут быть достигнуты относительно малыми изменениями исходного кода. Это также означает, что вы будете писать код быстрее; вы будете писать меньше кода и не будете терять время на поиск пути для выражения своих идей в ограничениях, накладываемых языком программирования [2].

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

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

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

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

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

<p>Как это началось?</p>

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

Маккарти был (и есть) исследователем в области искусственного интеллекта, и многие особенности, которые он заложил в первую версию, сделало этот язык замечательным инструментом для программирования искусственного интеллекта. Во время бума ИИ в 80-е Lisp оставался излюбленным языком для решения сложных проблем, как то: автоматическое доказательство теорем, планирование и составление расписаний, компьютерное зрение. Это были проблемы, требующие сложных программ, для написания которых нужен был мощный язык, так что программисты ИИ сделали Lisp таковым. Помогла и Холодная война, т.к. Пентагон выделял деньги Управлению перспективных исследовательских программ (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 содержит богатую среду исполнения с доступом к графическому пользовательскому интерфейсу, многопоточности, сокетам TCP/IP и многому другому. В наши дни Common Lisp эволюционирует, как и большинство других языков с открытым кодом: люди, использующие его, пишут библиотеки, которые им необходимы, и часто делают их доступными для всего сообщества. В последние годы, в частности, замечается усиление активности в разработке для Lisp библиотек с открытым кодом.

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

<p>Но я изучал Lisp раньше, и он не был тем, что вы описываете!</p>

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

<p>Для кого эта книга?</p>

Эта книга для вас, если вы интересуетесь Common Lisp, независимо от того, знаете ли вы его или просто хотите понять, из-за чего вокруг него разгорелась вся эта шумиха.

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

Если вы упёртый прагматик, желающий знать достоинства Common Lisp перед другими языками, такими, как Perl, Python, Java, C или C#, эта книга даст вам несколько идей по этому поводу. Или, может быть, вам нет никакого дела до использования Lisp и вы уверены, что он ничуть не лучше языков, которые вы уже знаете, но вам надоели заявления какого-нибудь Lisp-программиста, что вы просто не поняли его как следует. Если так, то в данной книге вы найдёте краткое введение в Common Lisp. Если после чтения этой книги вы по-прежнему будете думать, что Common Lisp ничем не лучше, чем ваши любимые языки, у вас будут веские обоснованные аргументы.

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

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

2
<p>2. Намылить, смыть, повторить: знакомство с REPL</p>

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

Этот набор предоставляет программисту современную среду разработки на Common Lisp, поддерживающую инкрементальный интерактивный стиль разработки, характерный для программирования на этом языке. Среда SLIME даёт дополнительное преимущество в виде унифицированного пользовательского интерфейса, не зависящего от выбранных вами операционной системы и реализации Common Lisp. В своей книге я буду ориентироваться на среду Lisp in a Box, но те, кто хочет изучить другие среды разработки, например, графические интегрированные среды разработки (IDE - Integrated Development Environment), предоставляемые некоторыми коммерческими поставщиками, или среды, основанные на других текстовых редакторах, не должны испытывать больших трудностей в понимании. [11]

<p>Выбор реализации Lisp</p>

Первое, что вам предстоит сделать выбрать реализацию Lisp. Это может показаться немного странным тем, кто раньше занимался программированием на таких языках как Perl, Python, Visual Basic (VB), C# или Java. Разница между Common Lisp и этими языками заключается в том, что Common Lisp определяется своим стандартом: не существует ни единственной его реализации, контролируемой "великодушным диктатором" (как в случае с Perl и Python), ни канонической реализации, контролируемой одной компанией (как в случае с VB, C# или Java). Любой желающий может создать свою реализацию на основе стандарта. Кроме того, изменения в стандарт должны вноситься в соответствии с процессом, контролируемым Американским Национальным Институтом Стандартов (ANSI). Этот процесс организован таким образом, что "случайные лица", такие, как частные поставщики программных решений, не могут вносить изменения в стандарт по своему усмотрению [12]. Таким образом, стандарт 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 [13] - высококачественная открытая реализация, способная компилировать в машинный код и работать на множестве различных UNIX-систем, включая Linux и OS X. SBCL "наследник" CMUCL [14] реализации Common Lisp, разработанной в университете Carnegie Mellon , и, как и CMUCL, является всеобщим достоянием (public domain, за исключением нескольких секций, покрываемых BSD-подобными (Berkley Software Distributions) лицензиями). CMUCL тоже хороший выбор, однако SBCL, обычно, легче в установке и поддерживает 21-разрядный Unicode [15]. OpenMCL будет отличным выбором для пользователей OS X: эта реализация способна компилировать в машинный код, поддерживать работу с потоками, а также прекрасно интегрируется с инструментальными комплектами Carbon и Cocoa. Кроме перечисленных, существуют и другие свободные и коммерческие реализации. Если вы захотите получить больше информации, в главе 32 вы найдёте список ресурсов.

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

<p>Введение в Lisp in a Box</p>

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.

<p>Освободите свой разум: Интерактивное программирование</p>

При запуске 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, так и при редактировании исходных файлов. Например, вы ведь не захотите каждый раз копировать и вставлять куски кода из редактора в REPL или перезагружать весь файл только потому, что изменилось одно определение, ваше окружение должно позволять вам вычислять или компилировать как отдельные выражения так и целые файлы из вашего редактора [16].

<p>Эксперименты в REPL</p>

Для знакомства с REPL, вам необходимо выражение Lisp, которое может быть прочитано, вычислено и выведено на экран. Простейшее выражение Lisp - это число. Если вы наберете 10 в приглашении Lisp и нажмете ВВОД, то сможете увидите что-то наподобие:

CL-USER> 10

10

Первая 10 - это то, что вы набрали. Считыватель Lisp, R в REPL, считывает текст "10" и создаёт объект Lisp, представляющий число 10. Этот объект - самовычисляемый объект, это означает, что такой объект при передаче в вычислитель, E в REPL, вычисляется сам в себя. Это значение подаётся на устройство вывода REPL, которое напечатает объект "10" в отдельной строке. Хотя это и похоже на сизифов труд, можно получить что-то поинтереснее, если дать интерпретатору Lisp пищу для размышлений. Например, вы можете набрать (+ 2 3) в приглашении Lisp.

CL-USER> (+ 2 3)

5

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

<p>"Здравствуй, Мир" в стиле Lisp</p>

Нет законченной книги по программированию без программы "Здравствуй, мир"("hello, world.") [17]. После того как интерпретатор запущен, нет ничего проще чем набрать строку "Здравствуй, мир".

CL-USER> "Здравствуй, мир"

"Здравствуй, мир"

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

Однако, наш пример не может квалифицироваться как программа "Здравствуй мир". Это, скорее, значение "Здравствуй мир".

Вы можете сделать шаг к настоящей программе, напечатав код, который, в качестве побочного эффекта, отправит на стандартный вывод строку "Здравствуй, мир". Common Lisp предоставляет несколько путей для вывода данных, но самый гибкий - это функция FORMAT. FORMAT получает переменное количество параметров, но только два из них обязательны: указание, куда осуществлять вывод, и строка для вывода. В следующей главе Вы увидите, как строка может содержать встроенные директивы, которые позволяют вставлять в строку последующие параметры функции (а-ля printf или строка % из Python). До тех пор, пока строка не содержит символа ~ , она будет выводиться как есть. Если вы передадите t в качестве первого параметра, функция FORMAT направит отформатированную строку на стандартный вывод. Итак, выражение FORMAT для печати "Здравствуй, мир" выглядит примерно так: [18]

CL-USER> (format t "Здравствуй, мир")

Здравствуй, мир

NIL

Стоит заметить, что результатом выражения FORMAT является NIL в строке после вывода "Здравствуй, мир". Этот NIL является результатом вычисления выражения FORMAT, напечатанного REPL. (NIL это Lisp-версия false и/или null. Подробнее об этом рассказывается в главе 4.) В отличие от других выражений, рассмотренных ранее, нас больше интересует побочный эффект выражения FORMAT (в данном случае, печать на стандартный вывод), чем возвращаемое им значение. Но каждое выражение в Lisp вычисляется в некоторый результат [19].

Однако, до сих пор остается спорным, написали ли мы настоящую программу. Но вы ведь здесь. Вы видите восходящий стиль программирования, поддерживаемый 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. Возвращаемое значение в этом случае - это имя только что определенной функции [20]. Но, подобно выражению FORMAT, это выражение более интересно своими побочными эффектами, нежели возвращаемым значением. Однако, в отличие от выражения FORMAT, побочные эффекты невидимы: после вычисления этого выражения создается новая функция, не принимающая аргументов, с телом (format t "hello, world" ) и ей дается имя HELLO-WORLD.

Теперь, после определения функции, вы можете вызвать ее следующим образом:

CL-USER> (hello-world)

hello, world

NIL

Вы можете видеть, что вывод в точности такой же, как при вычислении выражения FORMAT напрямую, включая значение NIL, напечатанное REPL. Функции в Common Lisp автоматически возвращают значение последнего вычисленного выражения.

<p>Сохранение вашей работы</p>

Вы могли бы утверждать, что это готовая программа "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 автоматически выровняет вторую строку соответствующим образом [21]:

(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), закроются [22]. Теперь перезапустите 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 означает, что загрузка всех определений произошла успешно [23]. Загрузка файла с помощью 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 миллионов миль от Земли) команда смогла произвести диагностику и исправление работающего кода, что позволило завершить эксперимент [24]. Один из программистов сказал об этом следующее:

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

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

3
<p>3. Практикум: Простая база данных</p>

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

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

Одно замечание по терминологии: в этой главе я расскажу о некоторых операторах Lisp. В главе 4 вы узнаете, что Common Lisp предоставляет три разных типа операторов: функции, макросы и операторы специального назначения. Для целей этой главы вам необязательно понимать разницу. Однако я буду ссылаться на различные операторы как на функции, макросы или специальные операторы, в зависимости от того, чем они на самом деле являются, вместо того, чтобы попытаться скрыть эти детали за одним словом оператор. Сейчас вы можете рассматривать функции, макросы и специальные операторы как более или менее эквивалентные сущности [25].

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

<p>CD и Записи</p>

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

Для начала вы можете остановиться на простом варианте и использовать список. Вы можете создать его с помощью функции LIST , которая, соответственно, возвращает список [26] из переданных аргументов.

CL-USER> (list 1 2 3)

(1 2 3)

Вы могли бы использовать четырёхэлементный список, отображающий позицию в списке на соответствующее поле записи. Однако другая существующая разновидность списков, называемая property list (список свойств) или, сокращенно, plist , в нашем случае гораздо удобнее. Plist это такой список, в котором каждый нечетный элемент является символом , описывающим следующий (чётный) элемент списка. На этом этапе я не буду углубляться в подробности понятия символ ; по своей природе это имя. Для символов, именующих поля в базе данных, мы можем использовать частный случай символов, называемый символами-ключами ( keyword symbol ). Ключ это имя, начинающееся с двоеточия (:), например, :foo [27]. Вот пример 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 говорит нам, [28] что эта запись определяет новую функцию. Имя функции 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)

<p>Заполнение CD</p>

Впрочем, создание одной записи ещё не создание базы данных. Вам необходима более комплексная структура данных для хранения записей. Опять же, простоты ради, список представляется здесь вполне подходящим выбором. Также для простоты вы можете использовать глобальную переменную *db*, которую можно будет определить с помощью макроса DEFVAR . Звездочки (*) в имени переменной это договоренность, принятая в языке Lisp при объявлении глобальных переменных. [29]

(defvar *db* nil)

Для добавления элементов в *db* можно использовать макрос PUSH . Но разумнее немного абстрагировать вещи и определить функцию '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 возвращает новое значение изменяемой им переменной. Таким образом, после каждого добавления порции данных вы видите содержимое вашей базы данных.

<p>Просмотр содержимого базы данных</p>

Вы также можете просмотреть текущее значение *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 формат строки; он может как содержать символьный текст, так и управляющие команды, контролирующие работу этой функции, например то, как она должна интерпретировать остальные аргументы. Команды, управляющие форматом вывода, начинаются со знака тильды ( ~ ) (так же, как управляющие команды printf начинаются с %). FORMAT может принимать довольно много таких команд, каждую со своим набором параметров [30]. Однако сейчас я сфокусируюсь только на тех управляющих командах, которые необходимы для написания функции 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*))

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

<p>Улучшение взаимодействия с пользователем</p>

Хотя функция 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. Макрос принимает набор выражений и вычисляет их по одному, слева направо. Если какое-нибудь из них дает истинное значение, то оно возвращается как результат макроса OR , а остальные не вычисляются. Если все выражения оказываются ложными, тогда макрос OR возвращает ложь ( 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

<p>Сохранение и загрузка базы данных</p>

Хорошо иметь удобный способ добавления записей в базу данных. Но пользователю вряд ли понравится заново добавлять все записи после каждого перезапуска 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 [31].

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

<p>Выполнение запросов к базе данных</p>

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

(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 не является именем функции это слово показывает, что вы определяете анонимную функцию [32]. Если не считать имени, 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 . В этом случае анонимная функция не просто избавляет вас от необходимости писать обычную функцию, она позволяет вам написать функцию, наследующую часть её значений FIXME содержимое поля 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-selector [33]. Функция выглядит странно, но она работает именно так, как нам нужно если вызвать 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, описание которых совпало по значению с аргументами переданными where [34]. Заметьте, что, чтобы задать ключ-параметр ripped , вам необходимо использовать список из трёх элементов, потому что вам нужно знать, действительно ли вызывающая функция передала ключ-параметр :ripped nil , означающее "Выбрать те CD, в поле ripped которых установлено значение nil ", либо опустила его, что означает "Мне всё равно, какое значение установлено в поле ripped ".

<p>Обновление существующих записей — повторное использование where</p>

Теперь, после того, как у вас есть достаточно универсальные функции select и where , очень логичной представляется реализация следующей возможности, которая необходима каждой базе данных, возможности обновления отдельных записей. В SQL команда update используется для обновления набора записей, удовлетворяющих FIXME конкретному условию where . Эта модель кажется хорошей, особенно когда у вас уже есть генератор условий where . Фактически, функция update применение некоторых идей, которые вы уже видели: использование передаваемого выражения выбора для указания записей, подлежащих обновлению, и использование аргументов-ключей для задания нового значения. Новая вещь здесь использование функции MAPCAR , которая отображает [35] список, в нашем случае это *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*)))

Ещё одна новинка в этой функции [36] приложение SETF к сложной форме вида (getf row :title) . Я расскажу о SETF подробнее в главе 6, но сейчас вам просто нужно знать, что это общий оператор присваивания, который может использоваться для присваивания друг другу различных "вещей", а не только переменных. (То, что SETF и GETF имеют настолько похожие имена просто совпадение. Между ними нет никакой особой взаимосвязи). Сейчас достаточно знать, что после выполнения (setf (getf row :title) title) у списка свойств, на который ссылается row , значением переменной, следующей за именем свойства :title , будет title. С помощью функции update , если вы решите, что действительно любите творчество Dixie Chicks, и что все их альбомы должны быть оценены в 11 баллов, можете выполнить следующую форму [37]:

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 [38] фактически изменяет содержимое базы данных [39].

<p>Избавление от дублирующего кода и большой выигрыш</p>

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

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

(if title (equal (getf cd :title) title) t)

Сейчас это не так плохо, но, как и во многих случаях дублирования кода, за это всегда приходится платить одну цену: если вы хотите изменить работу этого кода, вам нужно изменять множество копий. И если вы изменили поля в CD, вам придётся добавить или удалить условия для where . update страдает точно таким же дублированием. Это, несомненно, плохо, так как весь смысл функции where заключается в динамической генерации куска кода, проверяющего нужные нам значения; почему она должна производить работу во время выполнения, каждый раз проверяя, было ли ей передано значение title ?

Представьте, что вы попытались оптимизировать этот код и обнаружили, что много времени тратится на проверку того, заданы ли значения title и оставшиеся ключ-параметры [41]. Если вы на самом деле хотите избавиться от этих проверок во время выполнения, вы можете просмотреть программу и найти все места, где вы вызываете 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 макрос, в сущности, является генератором кода, который автоматически запускается для вас компилятором [42]. Когда выражение на 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.

<p>Об упаковке</p>

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

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

Целью этой главы являлось быстрое введение в лишь малую часть возможностей Lisp и демонстрация того, как они используются для написания кода, чуть более интересного, чем "Hello, world" . В следующей главе мы начнём более систематический обзор Lisp.

4
<p>4. Синтаксис и семантика</p>

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

<p>Почему так много скобок?</p>

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

Я не смогу действительно объяснить вам все состояние дел с синтаксисом Lisp, пока не расскажу немного подробней о макросах Lisp. Но я могу начать с предыстории, которая наводит на мысль, что имеет смысл откинуть предрассудки и попытаться разобраться, что к чему: когда John McCarthy изобрел Lisp, он намеревался реализовать его в более Algol-подобном синтаксисе, который он называл M-выражения. Однако он так не и сделал этого. Причину он объясняет в своей статье "История Lisp" [43].

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

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

<p>Разделение черного ящика</p>

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

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

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

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

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

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

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

<p>S-выражения</p>

Базовыми элементами s-выражения являются списки и атомы. Списки ограничиваются скобками и могут содержать любое число разделенных пробелами элементов. Все, что не список, является атомом [47]. Элементами списков в свою очередь также являются 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* представляются объектами, называющимися символами . Процедура чтения ничего не знает о том, как данное имя будет использоваться является ли оно именем переменной, функции или чем-то еще. Она просто читает последовательность знаков и создает объект, представляющий имя [48]. Почти любой знак может входить в имя. Однако, это не может быть пробельный знак, так как пробелом разделяются элементы списка. Цифры могут входить в имена, если имя целиком не сможет интерпретироваться как число. Схожим образом, имена могут содержать точки, но процедура чтения не может прочитать имя, состоящее только из точек. Существует десять знаков, которые не могут входить в имена, так как предназначены для других синтаксических целей: открывающая и закрывающая скобки, двойные и одинарные кавычки, обратный апостроф, запятая, двоеточие, точка с запятой, обратный слеш и вертикальная черта. Но даже эти знаки могут входить в имена, если их экранировать обратным слешем или окружить часть имени, содержащую знаки, которые нужно экранировать, с помощью вертикальных линий.

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

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

Так как имена в 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"))

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

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

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

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

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

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

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

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

<p>Вызовы функций</p>

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

(function-name argument*)

Таким образом, следующее выражение вычисляется путем первоначального вычисления 1, затем 2, а затем передачи результатов вычислений в функцию +, которая возвращает 3:

(+ 1 2)

Более сложное выражение, такое как следующее, вычисляется схожим образом за исключением того, что вычисление аргументов (+ 1 2) и (- 3 4) влечет за собой вычисление аргументов этих форм и применение соответствующих функций к ним:

(* (+ 1 2) (- 3 4))

В итоге, значения 3 и -1 передаются в функцию *, которая возвращает -3.

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

<p>Специальные операторы</p>

Нужно сказать, что не все операции могут быть определены как функции. Так как все аргументы функции вычисляются перед ее вызовом, не существует возможности написать функцию, которая ведет себя как оператор 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, но только малая их часть напрямую используется в ежедневном программировании [55].

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

Правило для 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 [56]

QUOTE используется достаточно часто, поэтому для него в процедуру чтения был встроен специальный синтаксис. Вместо написания такого:

(quote (+ 1 2))

вы можете написать это:

'(+ 1 2)

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

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

(let ((x 10)) x)

<p>Макросы</p>

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

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

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

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

<p>Истина, Ложь и Равенство</p>

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

Код в этой книге написан в стиле "всегда используйте EQL " [60].

Другие два предиката равенства 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 .

<p>Форматирование кода Lisp</p>

Хотя форматирование кода, строго говоря, не имеет ни синтаксического, ни семантического значения, хорошее форматирование важно для легкого чтения и написания кода. Ключевым моментов в форматировании кода 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))))

Выравнивание веток then и else , перенесенных под условие вместо того, чтобы находиться чуть правее 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: функции, переменные и макросы. Следующим шагом станут функции.

5
<p>5. Функции</p>

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

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

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

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

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

(defun name (parameter*)

"Optional documentation string."

тело-функции*)

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

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

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

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

В главе 2 мы написали функцию hello-world , которая выглядела вот так:

(defun hello-world () (format t "hello, world"))

Теперь вы можете проанализировать части этой функции. Она называется hello-world , список параметров пуст, потому что она не принимает аргументов, в ней нет строки документации, и ее тело состоит из одного выражения:

(format t "hello, world")

Вот пример немного более сложной функции:

(defun verbose-sum (x y)

"Sum any two numbers after printing a message."

(format t "Summing ~d and ~d.~%" x y)

(+ x y))

Эта функция называется verbose-sum , получает два аргумента, которые связываются с параметрами x и y , имеет строку документации, и ее тело состоит из двух выражений. Значение, возвращенное вызовом функции + , становится значением функции verbose-sum .

<p>Списки параметров функций</p>

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

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

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

<p>Необязательные параметры</p>

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

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

<p>Остаточные (Rest) параметры</p>

Необязательные параметры применяются только тогда, когда у вас есть отдельные параметры, для которых пользователь может указывать или не указывать значения. Но некоторые функции могут требовать изменяемого количества аргументов. Некоторые встроенные функции, которые вы уже видели, работают именно так. Функция 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 . [66] Хех! Этот мозгодробительный подход явно не является хорошим стилем написания программ.

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

(defun format (stream string &rest values) ...)

(defun + (&rest numbers) ...)

<p>Именованые параметры =</p>

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

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

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

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

(defun foo (&key a b c)

(list a b c))

Когда функция вызывается, каждый именованный параметр связывается со значением, которое указано после ключевого слова, имеющего то же имя, что и параметр. Вернемся к главе 4, в которой указывалось, что ключевые слова это имена, которые начинаются с двоеточия, и которые автоматически определяются как константы, вычисляемые сами в себя FIXME (self-evaluating).

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

(foo) ==> (NIL NIL NIL)

(foo :a 1) ==> (1 NIL NIL)

(foo :b 1) ==> (NIL 1 NIL)

(foo :c 1) ==> (NIL NIL 1)

(foo :a 1 :c 3) ==> (1 NIL 3)

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

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

Также как и для необязательных параметров, именованные параметры могут задавать выражение для вычисления значения по умолчанию и имя supplied-p -переменной. И для необязательных, и для именованных параметров, значение по умолчанию может ссылаться на параметры, указанные ранее в списке.

(defun foo (&key (a 0) (b 0 b-supplied-p) (c (+ a b)))

(list a b c b-supplied-p))

(foo :a 1) ==> (1 0 1 NIL)

(foo :b 1) ==> (0 1 1 T)

(foo :b 1 :c 4) ==> (0 1 4 T)

(foo :a 2 :b 1 :c 4) ==> (2 1 4 T)

Также, если по некоторым причинам вы хотите, чтобы пользователь использовал имена аргументов, отличающиеся от имен параметров, то вы можете заменить имя параметра на список, содержащий имя, которое будет использоваться пользователем при вызове, и имя параметра. Следующее определение foo :

(defun foo (&key ((:apple a)) ((:box b) 0) ((:charlie c) 0 c-supplied-p))

(list a b c c-supplied-p))

позволяет пользователю вызывать функцию вот так:

(foo :apple 10 :box 20 :charlie 30) ==> (10 20 30 T)

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

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

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

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

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

(list x y z))

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

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

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

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

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

(foo 1 :z 3) ==> ERROR

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

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

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

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

(list rest a b c))

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

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

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

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

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

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

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

(defun foo (n)

(dotimes (i 10)

(dotimes (j 10)

(when (> (* i j) n)

(return-from foo (list i j))))))

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

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

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

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

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

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

FOO

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

CL-USER> (function foo)

#<Interpreted Function FOO>

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

CL-USER> #'foo

#<Interpreted Function FOO>

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

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

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

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

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

(defun plot (fn min max step)

(loop for i from min to max by step do

(loop repeat (funcall fn i) do (format t "*"))

(format t "~%")))

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

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

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

*

*

**

****

*******

************

********************

*********************************

******************************************************

NIL

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

(plot

(first plot-data)

(second plot-data)

(third plot-data)

(fourth plot-data))

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

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

(apply #'plot plot-data)

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

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

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

<p>Анонимные функции</p>

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

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

(lambda (parameters) body)

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

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

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

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

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

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

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

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

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

**

****

******

********

**********

************

**************

****************

******************

********************

NIL

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

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

**

****

******

********

**********

************

**************

****************

******************

********************

NIL

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

6
<p>6. Переменные</p>

Следующим базовым строительным блоком, с которым нам нужно ознакомиться, переменные. Common Lisp поддерживает два вида переменных: лексические и динамические [74]. Эти два типа переменных примерно соответствуют "локальным" и "глобальным" переменным других языков. Однако, это соответствие лишь приблизительно. С одной стороны "локальные" переменные некоторых языков в действительности гораздо ближе к динамическим переменным Common Lisp [75]. И, с другой, локальные переменные некоторых других языков имеют лексическую область видимости не предоставляя всех возможностей, предоставляемых лексическими переменными Common Lisp. В частности, не все языки, предоставляющие переменные, имеющие лексическую область видимости, поддерживают замыкания.

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

<p>Основы переменных</p>

Как и в других языках, в Common Lisp переменные являются именованными местами, которые могут содержать значения. Однако, в Common Lisp переменные не типизированы таким же образом, как в таких языках, как Java или C++. То есть вам не нужно описывать тип объектов, которые может содержать каждая переменная. Вместо этого переменная может содержать значения любого типа и сами значения содержат информацию о типе, которая может быть использована для проверки типов во время выполнения. Таким образом, Common Lisp является динамически типизированным : ошибки типов выявляются динамически. Например, если вы передадите не число в функцию +, Common Lisp сообщит вам об ошибке типов. С другой стороны, Common Lisp является строго типизированным языком в том смысле, что все ошибки типов будут обнаружены: нет способа представить объект в качестве экземпляра класса, которым он не является [76].

Все значения в Common Lisp, по крайней мере концептуально, являются ссылками на объекты [77]. Поэтому присваивание переменной нового значения изменяет то, на какой объект ссылается переменная (то есть, куда ссылается переменная), но не оказывает никакого влияния на объект, на который переменная ссылалась ранее. Однако, если переменная содержит ссылку на изменяемый объект, вы можете использовать данную ссылку для изменения этого объекта, и это изменение будет видимо любому коду, который имеет ссылку на этот же объект.

Один из способов введения новой переменной вы уже использовали при определении параметров функции. Как вы видели в предыдущей главе, при определении функции с помощью DEFUN список параметров определяет переменные, которые будут содержать аргументы, переданные функции при вызове. Например, следующая функция определяет три переменные для хранения своих аргументов: x, y и z.

(defun foo (x y z) (+ x y z))

При каждом вызове функции Lisp создает новые привязки ( bindings ) для хранения аргументов, переданных при вызове этой функции. Привязка является проявлением переменной во время выполнения. Отдельная переменная сущность, на которую вы можете сослаться в исходном коде своей программы может иметь множество различных привязок за время выполнения программы. Отдельная переменная даже может иметь множество привязок в одно и то же время: параметры рекурсивной функции, например, связываются заново (rebound) при каждом вызове это функции.

Другой формой, позволяющей вводить новые переменные, является специальный оператор LET . Шаблон формы LET имеет следующий вид:

(let (variable*)

body-form*)

где каждая variable является формой инициализации переменной. Каждая форма инициализации является либо списком, содержащим имя переменной и форму начального значения, либо, как сокращение для инициализации переменной в значение NIL , просто именем переменной. Следующая форма LET , например, связывает три переменные x, y и z с начальными значениями 10, 20 и NIL :

(let ((x 10) (y 20) z)

...)

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

Значение последнего выражения тела возвращается как значение выражения LET . Как и параметры функций, переменные, вводимые LET , связываются заново (rebound) каждый раз, когда поток управления заходит в LET [78].

Область видимости ( scope ) параметров функций и переменных LET область программы, где имя переменной может быть использовано для ссылки на привязку переменной ограничивается формой, которая вводит переменную. Такая форма (определение функции или LET ) называется связывающей формой ( binding form ). Как вы скоро увидите, два типа переменных (лексические и динамические) используют два несколько отличающихся механизма области видимости, но в обоих случаях область видимости ограничена связывающей формой.

Если вы записываете вложенные связывающие формы, которые вводят переменные с одинаковыми именами, то привязки внутренних переменных скрывают внешние привязки. Например, при вызове следующей функции для параметра x создается привязка для хранения аргумента функции. Затем первая LET создает новую привязку с начальным значением 2, а внутренняя LET создает еще одну привязку с начальным значением 3. Комментарии справа указывают область видимости каждой привязки.

(defun foo (x)

(format t "Параметр: ~a~%" x) ; |<------ x - аргумент

(let ((x 2)) ; |

(format t "Внешний LET: ~a~%" x) ; | |<---- x = 2

(let ((x 3)) ; | |

(format t "Внутренний LET: ~a~%" x)) ; | | |<-- x = 3

(format t "Внешний LET: ~a~%" x)) ; | |

(format t "Параметр: ~a~%" x)) ; |

Каждое обращение к x будет ссылаться на привязку с наименьшей окружающей областью видимости. Как только поток управления покидает область видимости какой-то связывающей формы, привязка из непосредственно окрущающей области видимости перестает скрываться и x ссылается уже на нее. Таким образом, результатом вызова foo будет следующий вывод:

CL-USER> (foo 1)

Параметр: 1

Внешний LET: 2

Внутренний LET: 3

Внешний LET: 2

Параметр: 1

NIL

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

Например, в главе 7 вы встретите цикл DOTIMES , простой цикл-счетчик. Он вводит переменную, которая содержит значение счетчика, увеличивающегося на каждой итерации цикла. Например, следующий цикл, печатающий числа от 0 до 9, связывает переменную x:

(dotimes (x 10) (format t "~d " x))

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

(let* ((x 10)

(y (+ x 10)))

(list x y))

но не так:

(let ((x 10)

(y (+ x 10)))

(list x y))

Однако, вы можете добиться такого же результата при помощи вложенных LET .

(let ((x 10))

(let ((y (+ x 10)))

(list x y)))

<p>Лексические переменные и замыкания</p>

По умолчанию все связывающие формы в Common Lisp вводят переменные лексической области видимости ( lexically scoped ). На переменные лексической области видимости можно ссылаться только в коде, который текстуально находится внутри связывающей формы. Лексическая область видимости должна быть знакома каждому, кто программировал на Java, C, Perl или Python, так как все они предоставляют "локальные" переменные, имеющие лексическую область видимости. Программисты на Algol также должны чувствовать себя хорошо, так как этот язык первым ввел лексическую область видимости в 1960-х.

Однако, лексические переменные Common Lisp несколько искажают понятие лексической переменной, по крайней мере в сравнении с оригинальной моделью Algol. Это искажение проявляется при комбинировании лексической области видимости со вложенными функциями. По правилам лексической области видимости, только код, текстуально находящийся внутри связывающей формы, может ссылаться на лексическую переменную. Но что произойдет, когда анонимная функция содержит ссылку на лексическую переменную из окружающей области видимости? Например, в следующем выражении:

(let ((count 0)) #'(lambda () (setf count (+ 1 count))))

ссылка на count внутри формы LAMBDA допустима в соответствии с правилами лексической области видимости. Однако, анонимная функция, содержащая ссылку, будет возвращена как значение формы LET , и она может быть вызвана с помощью FUNCALL кодом, который не находится в области видимости LET . Так что же произойдет? Как выясняется, если count является лексической переменной, все работает. Привязка count, созданная когда поток управления зашел в форму LET , остается столько, сколько это необходимо, в данном случае до тех пор, пока что-то сохраняет ссылку на функциональный объект, возвращенный формой LET . Анонимная функция называется замыканием ( closure ), потому что она "замыкается вокруг" привязки, созданной LET .

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

(defparameter *fn* (let ((count 0)) #'(lambda () (setf count (1+ count)))))

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

CL-USER> (funcall *fn*)

1

CL-USER> (funcall *fn*)

2

CL-USER> (funcall *fn*)

3

Отдельное замыкание может "замыкаться вокруг" нескольких привязок переменных просто ссылаясь на них. Также множество замыканий могут захватывать одну и ту же привязку. Например, следующее выражение возвращает список трех замыканий, первое из которых увеличивает значение привязки count, вокруг которой оно "замкнуто", второе уменьшает его, а третье возвращает текущее значение:

(let ((count 0))

(list

#'(lambda () (incf count))

#'(lambda () (decf count))

#'(lambda () count)))

<p>Динамические (специальные) переменные</p>

Привязки с лексической областью видимости помогают поддерживать код понятным путем ограничения области видимости, в которой, буквально говоря, данное имя имеет смысл. Вот почему большинство современных языков программирования используют лексическую область видимости для локальных перменных. Однако, иногда вам действительно может понадобиться глобальная переменная переменная, к который вы можете обратиться из любой части своей программы. Хотя неразборчивое использование глобальных переменных может привести к "спагетти-коду" также быстро как и неумеренное использование goto, глобальные переменные имеют разумное использование и существуют в том или ином виде почти в каждом языке программирования [79]. И, как вы сейчас увидите, глобальные переменные Lisp, динамические переменные, одновременно и более удобны, и более гибки.

Common Lisp предоставляет два способа создания глобальных переменных: DEFVAR и DEFPARAMETER . Обе формы принимают имя переменной, начальное значение и опциональную строку документации. После создания переменной с помощью DEFVAR или DEFPARAMETER имя может быть использовано где угодно для ссылки на текущую привязку этой глобальной переменной. Как вы заметили в предыдущих главах, по соглашению глобальные переменные именуются именами, начинающимися и заканчивающимися *. Далее в этой главе вы увидите почему очень важно следовать этому соглашению по именованию. Примеры DEFVAR и DEFPARAMETER выглядят следующим образом:

(defvar *count* 0

"Число уже созданных виджетов.")

(defparameter *gap-tolerance* 0.001

"Допустимое отклонение интервала между виджетами.")

Различие между этими двумя формами состоит в том, что DEFPARAMETER всегда присваивает начальное значение названной переменной, а DEFVAR делает это только если переменная не определена. Форма DEFVAR также может использоваться без начального значения для определения глобальной переменной без установки ее значения. Такая переменная называется несвязанной ( unbound ).

На деле вам следует использовать DEFVAR для определения переменных, которые будут содержать данные, которые вы хотите сохранять даже при изменениях исходного кода, использующего эту переменную. Например, представьте, что две переменные, определенные ранее, являются частью приложения управления "фабрикой виджетов" [80]. Правильным будет определить переменную *count* с помощью DEFVAR , так как число уже созданных виджетов не становится недействительным лишь потому, что мы сделали некоторые изменения в коде создания виджетов [81].

С другой стороны, переменная *gap-tolerance* вероятно влияет некоторым образом на поведение самого кода создания виджетов. Если вы решите, что вам нужно меньшее или большее допустимое отклонение и, следовательно, измените значение в форме DEFPARAMETER, вы захотите, чтобы изменение вступило в силу при перекомпиляции и перезагрузке файла.

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

(defun increment-widget-count () (incf *count*))

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

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

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

Это работает замечательно, пока вы не забудете восстановить исходное значение *standard-output* после завершения действий. Если вы забудете восстановить *standard-output*, весь остальной код программы, использующий *standard-output*, также будет слать свой вывод в файл [82].

Но похоже, что то, что вам действительно нужно, это способ обернуть часть кода во что-то говорящее: "Весь нижележащий код (все функции, которые он вызывает, все функции, которые вызывают эти функции, и так далее до функций самого низкого уровня) должны использовать это значение для глобальной переменной *standard-output*". А затем, по завершении работы функции верхнего уровня, старое значение *standard-output* должно быть автоматически восстановлено.

Оказывается, что это именно те возможности, что предоставляет вам другой вид переменных Common Lisp: динамические переменные. Когда вы связываете динамическую переменную, например с LET -переменной или с параметром функции, привязка, создаваемая во время входа в связывающую форму, заменяет глобальную привязку на все время выполнения связывающей формы. В отличие от лексических привязок, к которым можно обращаться только из кода, находящегося в лексической области видимости связывающей формы, к динамическим привязкам можно обращаться из любого кода, вызываемого во время выполнения связывающей формы [83]. И оказывается, что все глобальные переменные на самом деле являются динамическими.

Таким образом, если вы хотите временно переопределить *standard-output*, это можно сделать просто пересвязав ее, например, с помощью LET .

(let ((*standard-output* *some-other-stream*))

(stuff))

В любом коде, который выполняется в результате вызова stuff, ссылки на *standard-output* будут использовать привязку, установленную с помощью LET . А после того как stuff завершится и поток управления покинет LET , новая привязка *standard-output* исчезнет и последующие обращения к *standard-output* будут видеть привязку, бывшую до LET . В любой момент времени самая последняя установленная привязка скрывает все остальные. Можно представить, что каждая новая привязка данной динамической переменной помещается в стек привязок этой переменной и ссылки на эту переменную всегда используют последнюю установленную привязку. После выхода из связывающей формы созданные в ней привязки убираются из стека, делая видимыми предыдующие привязки [84].

Простой пример показывает как это работает:

(defvar *x* 10)

(defun foo () (format t "X: ~d~%" *x*))

DEFVAR создает глобальную привязку переменной *x* со значением 10. Обращение к *x* в foo будет искать текущую привязку динамически. Если вы вызовете foo на верхнем уровне (from the top level), глобальная привязка, созданная DEFVAR , будет единственной доступной привязкой, поэтому будет напечатано 10.

CL-USER> (foo)

X: 10

NIL

Но вы можете использовать LET для создания новой привязки, которая временно скроет глобальную привязку, и foo напечатает другое значение.

CL-USER> (let ((*x* 20)) (foo))

X: 20

NIL

Теперь снова вызовем foo без LET , она опять будет видеть глобальную привязку.

CL-USER> (foo)

X: 10

NIL

Теперь определим новую функцию.

(defun bar ()

(foo)

(let ((*x* 20)) (foo))

(foo))

Обратите внимание, что средний вызов foo находится внутри LET , которая связывает *x* с новым значением 20. При вызове bar вы получите следующий результат:

CL-USER> (bar)

X: 10

X: 20

X: 10

NIL

Как вы можете заметить, первый вызов foo видит глобальную привязку со значением 10. Средний вызов видит новую привязку со значением 20. А после LET , foo снова видит глобальную привязку.

Как и с лексической привязкой, присваивание нового значения влияет только на текущую привязку. Чтобы увидеть это, вы можете переопределить foo, добавив присваивание значения переменной *x*.

(defun foo ()

(format t "Перед присваиванием~18tX: ~d~%" *x*)

(setf *x* (+ 1 *x*))

(format t "После присваивания~18tX: ~d~%" *x*))

Теперь foo печатает значение *x*, увеличивает его на единицу, а затем печатает его снова. Если вы просто запустите foo, вы увидите следующее:

CL-USER> (foo)

Перед присваиванием X: 10

После присваивания X: 11

NIL

Ничего удивительного. Теперь запустим bar.

CL-USER> (bar)

Перед присваиванием X: 11

После присваивания X: 12

Перед присваиванием X: 20

После присваивания X: 21

Перед присваиванием X: 12

После присваивания X: 13

NIL

Обратите внимание, начальное значение *x* равно 11: предыдущий вызов foo действительно изменил глобальное значение. Первый вызов foo из bar увеличивает глобальную привязку до 12. Средний вызов не видит глобальную привязку из-за LET . А затем последний вызов снова может видеть глобальную привязку и увеличивает ее с 12 до 13.

Так как это работает? Как LET знает, когда связывает *x*, что подразумевается создание динамической привязки вместо обычной лексической? Она знает, так как имя было объявлено специальным [85]. Имя каждой переменной, определенной с помощью DEFVAR и DEFPARAMETER автоматически глобально объявляется специальным. Это означает, что когда бы вы не использовали это имя в связывающей форме (в форме LET , или как параметр функции, или в любой другой конструкции, которая создает новую привязку переменной, вновь создаваемая привязка будет динамической. Вот почему *соглашение* *по* *именованию* так важно: будет не очень хорошо, если вы используете имя, о котором вы думаете как о лексической переменной, а эта переменная окажется глобальной специальной. С одной стороны, код, который вы вызываете, сможет изменить значение этой связи; с другой, вы сами можете скрыть связь, установленную кодом, находящимся выше по стеку. Если вы всегда будете именовать глобальные переменные, используя соглашение по именованию *, вы никогда случайно не воспользуетесь динамической связью, желая создать лексическую.

Также возможно локально объявить имя специальным. Если в связывающей форме вы объявите имя специальным, привязка, созданная для этой переменной, будет динамической, а не лексической. Другой код может локально определить имя специальным, чтобы обращаться к динамической привязке. Однако, локальные специальные переменные используются относительно редко, поэтому вам не стоит беспокоиться о них [86].

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

<p>Константы</p>

Еще одним видом переменных, вообще не упомянутых ранее, являются оксюморонические "константные переменные". Все константы являются глобальными и определяются с помощью DEFCONSTANT . Базовая форма DEFCONSTANT подобна DEFPARAMETER .

(defconstant name initial-value-form [ documentation-string ])

Как и в случае с DEFPARAMETER , DEFCONSTANT оказывает глобальный эффект на используемое имя: после этого имя может быть использовано только для обращения к константе; оно не может быть использовано как параметр функции или быть пересвязано с помощью любой другой связывающей формы. Поэтому многие программисты на Lisp следуют соглашению по именованию и используют для констант имена начинающиеся и заканчивающиеся +. Этому соглашению следуют немного в меньшей степени, чем соглашению для глобальных динамических имен, но оно является хорошей идеей по сходным причинам [87].

Еще на что нужно обратить внимание по поводу DEFCONSTANT это то, что в то время, как язык позволяет вам переопределять константы путем перевычисления DEFCONSTANT с другой формой начального значения, не определено то, что именно произойдет после такого переопределения. На практике большинство реализаций требуют, чтобы вы перевычислили любой код, ссылающийся на константу, чтобы изменение вступило в силу, так как старое значение могло быть встроено (inlined). Следовательно, правильным будет использовать DEFCONSTANT для определения только тех вещей, которые действительно являются константами, такие как значение NIL. Для вещей, которые вам может когда-нибудь понадобиться изменить, следует использовать DEFPARAMETER .

<p>Присваивание</p>

После создания привязки вы можете совершать с ней два действия: получить текущее значение и установить ей новое значение. Как вы видели в главе 4, символ вычисляется в значение переменной, которую он именует, поэтому вы можете получить текущее значение просто обратившись к переменной. Для присваивания нового значения привязке используйте макрос SETF , являющийся в Common Lisp оператором присваивания общего назначения. Базовая форма SETF следующая:

(setf place value)

Так как SETF является макросом, он может оценить форму "места", которому он осуществляет присваивание и расшириться (expand) в соответствующие низкоуровневые операции, осуществляющие необходимые действия. Когда "место" является переменной, этот макрос расширяется в вызов специального оператора SETQ , который, как специальный оператор, имеет доступ и к лексическим, и к динамическим привязкам [88]. Например, для присваивания значения 10 переменной x вы можете написать это:

(setf x 10)

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

(defun foo (x) (setf x 10))

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

(let ((y 20))

(foo y)

(print y))

напечатает 20, а не 10, так как именно оно является значением y , которое передается foo , где уже является значением переменной x перед тем, как SETF дает x новое значение.

SETF также может осуществить последовательное присваивание множеству "мест". Например, вместо следующего:

(setf x 1)

(setf y 2)

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

(setf x 1 y 2)

SETF возвращает присвоенное значение, поэтому вы можете вкладывать вызовы SETF как в следующем примере, который присваивает и x, и y одинаковое случайное значение:

(setf x (setf y (random 10)))

<p>Обобщенное присваивание</p>

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

Я опишу эти структуры данных в последующих главах, но так как мы рассматриваем присваивание, вы должны знать, что SETF может присвоить значение любому "месту". Когда я буду описывать различные составные структуры данных, я буду указывать, какие функции могут использоваться как "места, обрабатываемые SETF " (" SETFable places"). Кратко же можно сказать, что если вам нужно присвоить значение "месту", почти наверняка следует использовать SETF . Возможно даже расширить SETF для того, чтобы он мог осуществлять присваивание определенным пользователем "местам", хотя я не описываю такие возможности [89].

В этом отношении SETF не отличается от оператора присваивания = языков, произошедших от C. В этих языках оператор = присваивает новые значения переменным, элементам массивов, полям классов. В языках, таких как Perl и Python, которые поддерживают хэш-таблицы как встроенные типы данных, = может также устанавливать значения элементов хэш-таблицы. Таблица 6-1 резюмирует различные способы, которыми используется = в этих языках.

Таблица 6-1. Присваивание с помощью = в других языках программирования

Присваивание ... Java, C, C++ Perl Python ^ ... переменной x = 10; $x = 10; x = 10 | ... элементу массива a[0] = 10; $a[0] = 10; a[0] = 10 | ... элементу хэш-таблицы $hash{'key'} = 10; hash['key'] = 10 | ... полю объекта o.field = 10; $o->{'field'} = 10; o.field = 10 |

SETF работает сходным образом: первый "аргумент" SETF является "местом" для хранения значения, а второй предоставляет само значения. Как и с оператором = в этих языках, вы используете одинаковую форму и для выражения "места", и для получения значения [90]. Таким образом, эквиваленты вышеприведенных в таблице 6-1 присваиваний для Lisp следующие ( AREF функция доступа к массиву, GETHASH осуществляет операцию поиска в хэш-таблице, а field может быть функцией, которая обращается к слоту под именем field определенного пользователем объекта):

Простая переменная: (setf x 10)

Массив: (setf (aref a 0) 10)

Хэш-таблица: (setf (gethash 'key hash) 10)

Слот с именем 'field': (setf (field o) 10)

Обратите внимание, что присваиваение с помощью SETF "месту", которое является частью большего объекта, имеет ту же семантику, что и присваивание переменной: "место" модифицируется без оказания какого-либо влияния на объект, которых хранился там до этого. И вновь, это подобно тому, как ведет себя = в Java, Perl и Python [91].

<p>Другие способы изменения "мест"</p>

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

(setf x (+ x 1))

или уменьшить его так:

(setf x (- x 1))

Но это слегка утомительно по сравнению с стилем C: ++x и x. Вместо этого вы можете использовать макросы INCF и DECF , которые увеличивают и уменьшают "место" на определенную величину, по умолчанию 1.

(incf x) === (setf x (+ x 1))

(decf x) === (setf x (- x 1))

(incf x 10) === (setf x (+ x 10))

INCF и DECF являются примерами определенного вида макросов, называемых модифицирующими макросами ( modify macros ). Модифицирующие макросы являются макросами, построенными поверх SETF , которые модифицируют "места" путем присваивания нового значения, основанного на их текущем значении. Главным преимуществом таких макросов является то, что они более краткие, чем аналогичные операции, записанные с помощью SETF . Вдобавок, модифицирующие макросы определены таким образом, что делает их безопасными при использовании с "местами", когда выражение "места" должно быть вычислено лишь единожды. Несколько надуманным примером является следующее выражение, которое увеличивает значение произвольного элемента массива:

(incf (aref *array* (random (length *array*))))

Наивный перевод этого примера в выражение, использующее SETF , может выглядить следующим образом:

(setf (aref *array* (random (length *array*)))

(1+ (aref *array* (random (length *array*)))))

Однако, это не работает, так как два последовательных вызова RANDOM не обязательно вернут одинаковое значение: это выражение вероятно получит значение одного элемента массива, увеличит его, а затем сохранит его как новое значение другого элемента массива. Однако, выражение INCF сделает все правильно, так как знает, как правильно разобрать это выражение:

(aref *array* (random (length *array*)))

чтобы извлечь те части, которые возможно могут иметь побочные эффекты, и гарантировать, что они будут вычисляться лишь один раз. В этом случае, выражение INCF вероятно расширится в нечто более или менее подобное этому:

(let ((tmp (random (length *array*))))

(setf (aref *array* tmp) (1+ (aref *array* tmp))))

Вообще, модифицирующие макросы гарантируют однократное вычисление слева направо своих аргументов, а также подформ формы места (place form).

Макрос PUSH , который вы использовали в примере с базой данных для добавления элементов в переменную *db*, является еще одним модифицирующим макросом. Более подробно о его работе и работе POP и PUSHNEW будет сказано в главе 12, где я буду говорить о том, как представляются (represented) списки в Lisp.

И наконец, два слегка эзотерических, но полезных модифицирующих макроса ROTATEF и SHIFTF . ROTATEF циклически сдвигает значение между "местами". Например, если вы имеете две переменные, a и b, этот вызов:

(rotatef a b)

обменяет значения двух переменных и вернет NIL . Так как a и b являются переменными и вам не нужно беспокоиться о побочных эффектах, предыдущее выражение ROTATEF эквивалентно следующему:

(let ((tmp a)) (setf a b b tmp) nil)

С другими видами "мест" эквивалентное выражение с использованием SETF может быть более сложным.

SHIFTF подобен ROTATEF за исключением того, что вместо циклического сдвига значений, он просто сдвигает их влево: последний аргумент предоставляет значение, которое перемещается в предпоследний аргумент и так далее. Исходное значение первого аргумента просто возвращается. Таким образом, следующее:

(shiftf a b 10)

эквивалентно (и снова, так как вам не нужно беспокоиться о побочных эффектах) следующему:

(let ((tmp a)) (setf a b b 10) tmp)

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

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

7
<p>7. Макросы: Стандартные управляющие конструкции</p>

В то время, как многие из идей, появившиеся в Лиспе, от выражений условия до сборки мусора, были добавлены в другие языки, есть одна особенность языка, которая продолжает делать Common Lisp стоящим особняком от всех, это его система макросов. К несчастью, слово "макрос" описывает множество вещей в компьютерных науках, к которым макросы Common Lisp имеют неявное и метафорическое отношение FIXME. Это приводит к бесконечным недопониманиям, когда адепты Лиспа пытаются объяснить другим, насколько макросы замечательны. [92]

Чтобы понять макросы Лиспа, необходимо подойти к делу со свободной головой, без предубеждений, основанных на других вещах, которые также оказались названными словом "макросы". Итак, давайте начнём нашу тему с шага назад и обзора различных путей, которыми создаются расширения в языках.

Всем программистам должна быть привычна идея о том, что определение языка может включать стандартную библиотеку функций, которая строится на "ядре" языка библиотеку, которая могла бы быть написана посредством языка любым программистом, если бы она не была определена как часть стандартной библиотеки. Стандартная библиотека языка Си, например, может быть написана почти полностью на переносимом Си. Аналогично, большая часть всё растущего набора классов и интерфейсов, которые поставляются в стандартном наборе Java Development Kit (JDK), написаны на "чистом" Java.

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

В то время, как Common Lisp поддерживает оба этих метода расширения языка, макросы дают Лиспу ещё один путь. Как было упомянуто кратко в Главе 4, каждый макрос определяет свой собственный синтаксис определение того, как s-выражения, которые ему передаются, будут превращены в Лисп-формы. С помощью макросов, как части ядра языка, возможно создавать новые синтаксические управляющие конструкции, такие как WHEN , DOLIST и LOOP , а так же формы определений вроде DEFUN и DEFPARAMETER , как часть "стандартной библиотеки", вместо встраивания их в ядро. Это имеет свои последствия для реализации языка, но как программиста на Лисп, вас будет больше заботить то, что это даёт вам ещё один способ расширения языка, делая его языком, лучше подходящим для выражения решений ваших собственных программистских проблем.

В данный момент может показаться, что преимущества от наличия ещё одного пути расширения языка будет легко понять. Но по некоторой причине большое количество программистов, которые фактически не использовали макросы Лиспа и которые не задумываются о создании новых функциональных абстракций или определений новых иерархий классов для решения своих задач, панически боятся самой мысли о том, что они будут иметь возможность описания новых синтаксических абстракций. Наиболее общей причиной "макрофобии", похоже является плохой опыт от использования других "макросистем". Простой страх перед неизвестным несомненно так же играет определенную роль. Чтобы избежать макрофобических реакций, я буду постепенно вводить в предмет через обсуждение нескольких стандартных макросов, конструкций контроля, определённых в Common Lisp. Это те вещи, которые должны были быть встроены в ядро языка если бы в Лиспе не было макросов. Когда вы используете их, вам не надо беспокоиться, что они сделаны в виде макросов, но они представляют из себя хороший пример того, что вы можете сделать с помощью макросов. [93] В следующей главе я покажу вам, как вы можете определять свои собственные макросы.

<p>WHEN и UNLESS</p>

Как вы уже видели, наиболее базовую форму условного выражения если x, делай y; иначе делай z представляет специальный оператор IF , который имеет следующую базовую форму:

(if condition then-form [else-form])

condition вычисляется и, если его значение не NIL , тогда then-form выполняется и полученное значение возвращается. Иначе выполняется else-form , если она есть, и её значение возвращается. Если condition даёт NIL и нет else-form , тогда IF возвращает NIL .

(if (> 2 3) "Yup" "Nope") ==> "Nope"

(if (> 2 3) "Yup") ==> NIL

(if (> 3 2) "Yup" "Nope") ==> "Yup"

Однако, IF не является вообще-то такой уж замечательной синтаксической конструкцией, потому что then-form и else-form каждая ограничена одной лисп-формой. Это значит, что если вы хотите выполнить последовательность действий в каком-либо из этих случаев, вам надо обернуть их в какой-то другой синтаксис. Например, предположим, в середине программы спам-фильтра, вы захотите сохранить в файле сообщение, как спам, и обновить базу данных по спаму, если сообщение - спам. Вы не можете написать так:

(if (spam-p current-message)

(file-in-spam-folder current-message)

(update-spam-database current-message))

потому что вызов update-spam-database будет принят за случай else , а не как часть ветви then . Другой специальный оператор PROGN , выполняет любое число форм по порядку и возвращает значение последней формы. Так что вы могли бы получить желаемое, записав всё следующим образом:

(if (spam-p current-message)

(progn

(file-in-spam-folder current-message)

(update-spam-database current-message)))

Это не так уж и ужасно. Однако, учитывая количество раз, когда вам придётся использовать эту идиому, не трудно представить себе, что через некоторое время это станет утомительно. "Почему", вы можете спросить у себя, "Лисп не предоставляет возможность выразить то, что на самом деле мне надо, скажем 'Если x верно, делай то, то и ещё вот это'?" Другими словами, через некоторое время, вы заметите повторяемость сочетания IF плюс PROGN и захотите как-то абстрагироваться от этих деталей, вместо того, чтобы каждый раз заново переписывать их.

Это как раз то, что предоставляют макросы. В данном случае, Коммон Лисп поставляется со стандартным макросом WHEN , с которым всё можно написать так:

(when (spam-p current-message)

(file-in-spam-folder current-message)

(update-spam-database current-message))

Но если бы он не был встроен в стандартную библиотеку, вы могли бы определить WHEN самостоятельно, как макрос вот так, используя запись с обратной кавычкой, которую я обсуждал в Главе 3: [94]

(defmacro when (condition &rest body)

`(if ,condition (progn ,@body)))

Сопутствующим макросу WHEN является UNLESS , который оборачивает условие, выполняя формы из тела, только если условие ложно. Другими словами:

(defmacro unless (condition &rest body)

`(if (not ,condition) (progn ,@body)))

Конечно, это довольно тривиальные макросы. Тут нет никакой страшной чёрной магии; они просто абстрагируют некоторые детали языковой бухгалтерии, позволяя вам выражать свои намерения немного более ясно. Но их тривиальность имеет важное значение: так как система макросов встроена в язык, вы можете писать тривиальные макросы вроде WHEN и UNLESS , которые дают вам небольшую, но реальную выгоду в ясности, которая затем умножается в тысячу раз когда вы используете их. В Главах 24, 26 и 31 вы увидите, как макросы могут быть использованы для серьёзных вещей, создавая целый предметно-ориентированный (domain-specific), встроенный язык. Но сначала, давайте закончим наше обсуждение стандартных макросов управления.

<p>COND</p>

Другой раз, когда непосредственное IF выражение может оказаться ужасным, это когда у вас есть условное выражение с множественными ветвлениями: если A, делай X, иначе, если B, делай Y; иначе делай Z . Нет никакой логической проблемы в написании такой цепочки условных выражений с IF , но получится не очень красиво.

(if a

(do-x)

(if b

(do-y)

(do-z)))

И это будет выглядеть ещё более ужасно, если вам понадобится включить множество форм для then случаев, привлекая несколько PROGN . Так что ничего удивительного, что Коммон Лисп предоставляет макрос для выражения условия с множеством ветвлений: COND . Вот базовый вид:

(cond

(test-1 form*)

.

.

.

(test-N form*))

Каждый элемент в теле представляет одну ветвь условия и состоит из списка, содержащего форму условия и ноль или более форм для выполнения, если выбрана эта ветвь. Условия вычисляются в том порядке, в каком расположены ветви до тех пор, пока одно из них не даст истину. В этой точке, оставшиеся формы из ветви выполняются и значение последней формы ветви возвращается как результат работы всего COND . Если ветвь не содержит форм после условия, то возвращается само значение условия. По соглашению, ветвь представляющая последний случай else в цепочке if/else-if записывается с условием T . Подойдёт любое не- NIL значение, но T служит дорожным знаком при чтении кода. Таким образом вы можете записать предыдущее вложенное IF выражение, используя COND , вот так:

(cond (a (do-x))

(b (do-y))

(t (do-z)))

<p>AND, OR и NOT</p>

При написании условий в IF , WHEN , UNLESS и COND формах, три оператора оказываются очень полезны, это булевы логические операторы AND , OR и NOT .

NOT - это функция, которая строго говоря не относится к этой главе, но она очень тесно связана с AND и OR . Она берёт свой аргумент и обращает его значение истинности, возвращая T , если аргумент NIL и NIL в ином случае.

AND и OR , однако являются макросами. Они представляют логические конъюнкцию и дизъюнкцию произвольного числа подформ и определены как макросы, так что они оптимальны в выполнении. Это значит, что они вычисляют ровно столько своих подформ, в порядке слева направо, сколько необходимо для конечного значения. То есть AND останавливается и возвращает NIL сразу же, как только одна из подформ выдаст NIL . Если все подформы выдают не- NIL результат, она возвращает значение последней подформы. OR , с другой стороны, останавливается, как только одна из подформ выдаст не- NIL и возвращает полученное значение. Если ни одна из подформ не выдаст истину, OR возвращает NIL . Вот несколько примеров:

(not nil) ==> T

(not (= 1 1)) ==> NIL

(and (= 1 2) (= 3 3)) ==> NIL

(or (= 1 2) (= 3 3)) ==> T

<p>Циклы</p>

Циклические конструкции представляют собой важный тип управляющих конструкций(FIXME в оригинале сказано наоборот). Циклические средства в Коммон Лисп, в дополнение к мощности и гибкости, являются интересным уроком по программированию в стиле "получить всё и сразу", который позволяют макросы.

Как оказалось, ни один из 25 специальных операторов Лиспа не поддерживает напрямую структуру циклов. Все циклические конструкции контроля в Лиспе - это макросы, построенные на двух специальных операторах, которые представляют собой примитивное goto средство. [95] Как многие хорошие абстракции, синтаксические или нет, циклические макросы в Лиспе построены как набор слоёв абстракций, начиная с основы, которой являются те два специальных оператора.

В самом низу (оставляя в стороне специальные операторы) находится наиболее общая конструкция контроля DO . Хотя и очень мощный, DO страдает, как и многие абстракции общего назначения, от черезмерности для простых ситуаций. Так что Лисп предоставляет два других макроса DOLIST and DOTIMES , которые менее гибки, чем DO , но лучше поддерживают наиболее распространённые случаи цикла по элементам списка или цикла с подсчётом. Хотя реализация может реализовать эти макросы как ей угодно, обычно они реализованы как макросы, которые раскрываются в соответствующий DO цикл. Таким образом DO предоставляет базовую структурную конструкцию цикла поверх нижележащих примитивов, представленных специальными операторами Коммон Лиспа, а DOLIST и DOTIMES представляют две лёгких в использовании, хотя и менее общие конструкции. И, как вы увидите в следующей главе, вы можете строить свои собственные конструкции цикла поверх DO в ситуациях, где DOLIST и DOTIMES вам не подходят.

Наконец, макрос LOOP представляет собой полномасштабный мини-язык для выражения циклических конструкций на не Лиспо-, а англо-подобном (или, как минимум, Алголо-подобном) языке. Некоторые хакеры Лиспа любят LOOP ; другие ненавидят его. Фанаты LOOP любят его за то, что он предоставляет краткий способ выразить определённые, обычно необходимые циклические конструкции. Его недоброжелатели не любят его, потому что он недостаточно похож на остальной Лисп. Однако, к какому бы лагерю вы не примкнули, это замечательный пример возможностей макросов добавлять новые конструкции в язык.

<p>DOLIST и DOTIMES</p>

Я начну с лёгких для использования DOLIST и DOTIMES макросов.

DOLIST проходит по всем элементам списка, выполняя тело цикла с переменной, содержащей последовательно элементы списка. [96] Вот базовый скелет (оставляя некоторые эзотерические опции):

(dolist (var list-form)

body-form*)

Когда цикл стартует, list-form выполняется один раз, чтобы создать список. Затем тело цикла выполняется для каждого элемента в списке, с переменной var , содержащей значение элемента. Например:

CL-USER> (dolist (x '(1 2 3)) (print x))

1

2

3

NIL

Использованная таким образом, форма DOLIST , в целом, возвращает NIL .

Если вы хотите прервать цикл DOLIST до окончания списка, можете использовать RETURN .

CL-USER> (dolist (x '(1 2 3)) (print x) (if (evenp x) (return)))

1

2

NIL

DOTIMES - это конструкция цикла верхнего уровня для циклов с подсчётом. Основной вид более-менее такой же, как у DOLIST .

(dotimes (var count-form)

body-form*)

count-form должна выдать целое число. Каждый раз, в процессе цикла, var содержит последовательные целые от 0 до на единицу меньшего, чем то число. Например:

CL-USER> (dotimes (i 4) (print i))

0

1

2

3

NIL

Так же, как и с DOLIST , вы можете использовать RETURN , чтобы прервать цикл раньше.

Так как тела обоих DOLIST и DOTIMES циклов могут содержать любые типы выражений, вы так же можете делать циклы вложенными. Например, чтобы напечатать таблицу умножения от 1 x 1 = 1 да 20 x 20 = 400, вы можете написать такую пару вложенных циклов DOTIMES :

(dotimes (x 20)

(dotimes (y 20)

(format t "~3d " (* (1+ x) (1+ y))))

(format t "~%"))

<p>DO</p>

Хотя DOLIST и DOTIMES удобны и легки в использовании, они недостаточно гибки, чтобы использоваться для любых циклов. Например, что если вы захотите менять на каждом шаге несколько переменных параллельно? Или использовать произвольное выражение для проверки окончания цикла? Если ни DOLIST , ни DOTIMES не подходят для ваших целей, у вас всё ещё есть доступ к наиболее общему циклу DO .

Там, где DOLIST и DOTIMES предоставляют только одну переменную цикла, DO позволяет вам держать любое число переменных и даёт вам полный контроль над тем, как они будут изменяться на каждом шаге цикла. Вы так же определяете проверку, которая говорит когда циклу завершиться и может предоставлять форму для вычисления в конце цикла, чтобы сформировать возвращаемое значение для всего DO в целом. Базовый шаблон выглядит так:

(do (variable-definition*)

(end-test-form result-form*)

statement*)

Каждое variable-definition (определение переменной) вводит переменную, которая будет в поле видимости тела цикла. Полная форма одного определения переменной, это список, содержащий три элемента.

(var init-form step-form)

init-form будет выполнена в начале цикла и полученное значение присвоено переменной var . Перед каждой последующей итерацией цикла, step-form будет выполнена и её значение присвоено var . Форма step-form необязательна; если её не будет, переменная останется с тем же значением от итерации к итерации, пока вы прямо не назначите ей новое значение в теле цикла. Так же, как и с присвоением переменным значений в LET , если форма init-form не задана, переменной присваивается NIL . Так же, как и в LET , вы можете использовать просто имя переменной, вместо списка, содержащего только имя.

В начале каждой итерации, после того, как все переменные цикла получили свои новые значения, выполняется форма end-test-form . До тех пор, пока она вычисляется в NIL , итерация происходит, выполняя statement по порядку.

Когда вычисление формы end-test-form выдаст истину, будет вычислена форма result-forms и значение, полученное в результате, будет возвращено как значение всего выражения DO .

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

(do ((n 0 (1+ n))

(cur 0 next)

(next 1 (+ cur next)))

((= 10 n) cur))

шаговые формы (1+ n) , next , и (+ cur next) все вычисляются, использую старое значение n , cur и next . Только после вычисления всех шаговых форм, переменные получают свои новые значения. (Математически образованные читатели могут заметит, что это частично эффективный способ подсчёта одиннадцатого числа Фибоначчи.)

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

(do (variable-definition*)

(end-test-form result-form*)

statement*)

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

**(**do **(**(i 0 (1+ i))**)**

**(**(>= i 4)**)**

(print i)**)**

(DELETEME - не знаю как выделить жирным скобки в коде)

Заметьте, что форма для результата пропущена. Это, однако, не особо распространённое использование DO , так как такой цикл гораздо проще написать используя DOTIMES . [98]

(dotimes (i 4) (print i))

Другой пример, в котором отсутствует тело цикла для вычисления чисел Фибоначчи:

**(**do **(**(n 0 (1+ n))

(cur 0 next)

(next 1 (+ cur next))**)**

**(**(= 10 n) cur**))**

(DELETEME - не знаю как выделить жирным скобки в коде)

Наконец, следующий пример демонстрирует цикл DO , в котором нет привязанных переменных. Он крутится, пока текущее время меньше, чем значение глобальной переменной, печатая "Waiting" каждую минуту. Заметьте, что даже без переменных цикла, вы всё равно нуждаетесь в пустом списке для списка переменных.

**(**do **()**

**(**(> (get-universal-time) *some-future-date*)**)**

(format t "Waiting~%")

(sleep 60)**)**

(DELETEME - не знаю как выделить жирным скобки в коде)

<p>Всемогущий LOOP</p>

Для простых случаев у вас есть DOLIST и DOTIMES . И если они не удовлетворяют вашим нуждам, вы можете вернуться к совершенно общему DO . Чего ещё можно хотеть?

Однако оказывается, что удобные идиомы для циклов появляются снова и снова, такие, как циклы по элементам различных структур с данными: списков, векторов, хэш-таблиц и пакетов, либо накопление значений разными способами в процессе цикла: собирание, подсчёт, суммирование, минимизация или максимизация. Если вы нуждаетесь в цикле, который бы делал одну из этих вещей (или несколько одновременно), макрос LOOP может предоставить вам простой путь это выразить.

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

(loop

body-form*)

Формы внутри тела выполняются каждый раз в процессе цикла, который будет длиться вечно, пока вы не используете RETURN , чтобы прервать его. Например, вы могли бы записать предыдущий DO цикл с простым LOOP .

(loop

(when (> (get-universal-time) *some-future-date*)

(return))

(format t "Waiting~%")

(sleep 60))

Расширенный LOOP несколько иной зверь. Он отличается использованием специальных ключевых слов цикла, которые представляют язык специального назначения для выражения циклических идиом. FIXME(Это не по русски:) Это ничего не значит, что не все Лисповоды любят язык расширенного LOOP . Как минимум один из создателей Коммон Лиспа ненавидит его. Критики LOOP жалуются, что его синтаксис совсем не лисповский. (другими словами, в нём недостаточно скобок). Любители LOOP замечают, что в этом то и весь смысл: сложные циклические конструкции достаточно тяжелы для восприятия и без заворачивания их в туманный синтаксис DO . Лучше, говорят они, иметь немного более наглядный синтаксис, который давал бы вам какие-то подсказки о том, что происходит.

Вот, например, характерный DO цикл, который собирает числа от 1 до 10 в список:

(do ((nums nil) (i 1 (1+ i)))

((> i 10) (nreverse nums))

(push i nums)) ==> (1 2 3 4 5 6 7 8 9 10)

У опытного Лиспера не будет никаких проблем понять этот код это просто вопрос понимания основной формы DO цикла и распознания PUSH / NREVERSE идиомы для построения списка. Но это не совсем прозрачно. Версия с LOOP , с другой стороны, почти понятна как предложение на английском. (цикл по i от 1 до 10, собирая i )

(loop for i from 1 to 10 collecting i) ==> (1 2 3 4 5 6 7 8 9 10)

Далее, ещё несколько примеров простого использования LOOP . Вот сумма квадратов первых десяти чисел:

(loop for x from 1 to 10 summing (expt x 2)) ==> 385

Это подсчёт числа гласных в строке:

(loop for x across "the quick brown fox jumps over the lazy dog"

counting (find x "aeiou")) ==> 11

Вот вычисление одиннадцатого числа Фибоначчи, аналогично использованному ранее циклу DO :

(loop for i below 10

and a = 0 then b

and b = 1 then (+ b a)

finally (return a))

Символы across , and , below , collecting , counting , finally , for , from , summing , then и to являются некоторыми из ключевых слов цикла, чьё присутствие обозначает, что перед нами расширенная версия LOOP . [99]

Я приберегу подробности о LOOP для Главы 22, однако сейчас стоит заметить, что это ещё один пример того, как макросы могут быть использованы для расширения основы языка. В то время как LOOP предоставляет свой собственный язык для выражения циклических конструкций, он никак не отрезает вас от остального Лиспа. Ключевые слова LOOP разбираются в соответствии с его грамматикой, но остальной код внутри LOOP , это обычный Лисп-код.

И так же стоит отметить ещё раз, что хотя макрос LOOP гораздо более сложный, чем WHEN или UNLESS , он просто ещё один макрос. Если бы он не был включён в стандартную библиотеку, вы могли бы сделать это сами или взять стороннюю библиотеку, которая это сделает.

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

8
<p>8. Макросы: Создание собственных макросов</p>

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

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

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

<p>История Мака: обычная такая история</p>

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

В отчаянии большой босс нанял младшего (junior) программиста, Мака, чьей работой стал поиск комментариев, написание требуемого кода и вставка его в программу на место комментариев. Мак никогда не запускал программы, ведь они не были завершены и поэтому он попросту не мог этого сделать. Но даже если бы они были завершены, Мак не знал, какие данные необходимо подать на их вход. Поэтому он просто писал свой код, основываясь на содержимом комментариев, и посылал его назад создавшему комментарий программисту.

С помощью Мака все программы вскоре были доделаны, и компания заработала уйму денег продавая их: так много денег, что смогла удвоить количество программистов. Но по какой-то причине никто не думал нанимать кого-то в помощь Маку; вскоре он один помогал нескольким дюжинам программистов. Чтобы не тратить все свое время на поиск комментариев в исходном коде, Мак внес небольшие изменения в используемый программистами компилятор. Теперь, если компилятор встречал комментарий, то отсылал его электронной почтой Маку, а затем ждал ответа с замещающим комментарий кодом. К сожалению, даже с этими изменениями Маку было тяжело удовлетворять запросам программистов. Он работал так тщательно, как только мог, но иногда, особенно когда записи не были ясны, он допускал ошибки.

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

Следующее новшество появилось, когда программист вставил в самый верх одной из своих программ комментарий, содержащий определение функции и пояснение, гласившее: "Мак, не пиши здесь никакого кода, но сохрани эту функцию на будущее; я собираюсь использовать ее в некоторых своих комментариях." Другие комментарии в этой программе гласили следующее: "Мак, замени этот комментарий на результат выполнения той функции с символами x и y как аргументами."

Этот метод распространился так быстро, что в течение нескольких дней большинство программ стало содержать дюжины комментариев с описанием функций, которые использовались только кодом в других комментариях. Чтобы облегчить Маку различение комментариев, содержащих только определения и не требующих немедленного ответа, программисты отмечали их стандартным предисловием: "Definition for Mac, Read Only" (Определение для Мака, только для чтения). Это (как мы помним, программисты были очень ленивы) быстро сократилось до "DEF. MAC. R/O", а потом до "DEFMACRO".

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

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

<p>Время раскрытия макросов против времени выполнения</p>

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

Очень важно полностью понимать это различие, так как код, работающий во время раскрытия макросов, запускается в окружении, сильно отличающемся от окружения кода, работающего во время выполнения. А именно, во время раскрытия макросов не существует способа получить доступ к данным, которые будут существовать во время выполнения. Подобно Маку, который не мог запускать программы, над которыми он работал, так как не знал, что является корректным входом для них, код, работающий во время раскрытия макросов, может работать только с данными, являющимися неотъемлемой частью исходного кода. Для примера предположим, что следующий исходный код появляется где-то в программе:

(defun foo (x)

(when (> x 10) (print 'big)))

Обычно вы бы думали о x как о переменной, которая будет содержать аргумент, переданный при вызове foo. Но во время раскрытия макросов (например когда компилятор выполняет макрос WHEN ) единственными доступными данными является исходный код. Так как программа пока не выполняется, нет вызова foo и, следовательно, нет значения, ассоциированного с x . Вместо этого значения, которые компилятор передает в WHEN , являются списками Lisp, представляющими исходный код, а именно (> x 10) и (print 'big) . Предположим, что WHEN определен, как вы видели в предыдущей главе, подобным образом:

(defmacro when (condition &rest body)

`(if ,condition (progn ,@body)))

При компиляции кода foo макрос WHEN будет запущен с этими двумя формами в качестве аргументов. Параметр condition будет связан с формой (> x 10) , а форма (print 'big) будет собрана (will be collected) в список (и будет его единственным элементом), который станет значением параметра &rest body . Выражение квазицитирования затем сгенерирует следующий код:

(if (> x 10) (progn (print 'big)))

подставляя значение condition , а также вклеивая значение body в PROGN .

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

<p>DEFMACRO</p>

Как вы видели в главе 3, макросы на самом деле определяются с помощью форм DEFMACRO , что означает, разумеется, "DEFine MACRO", а не "Definition for Mac". Базовый шаблон DEFMACRO очень похож на шаблон DEFUN .

(defmacro name (parameter*)

"Optional documentation string."

body-form*)

Подобно функциям, макрос состоит из имени, списка параметров, необязательной строки документации и тела, состоящего из выражений Lisp [100]. Однако, как я только что говорил, работой макроса не является осуществление какого-то действия напрямую, его работой является генерирование кода, который затем сделает то, что вам нужно.

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

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

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

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

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

Подводя итог можно сказать, что шаги по написанию макросов следующие:

1. Написание примера вызова макроса, а затем кода, в который он должен быть раскрыт (или в обратном порядке).

2. Написание кода, генерирующего написанный вручную код раскрытия по аргументам в примере вызова.

3. Проверка того, что предоставляемамя макросом абстракция не "протекает".

<p>Пример макроса: do-primes</p>

Для того, чтобы увидеть, как этот трехшаговый процесс осуществляется, вы напишете макрос do-primes , который предоставляет конструкцию итерирования, подобную DOTIMES и DOLIST , за исключением того, что вместо итерирования по целым числам или элементам списка итерирование будет производиться по последовательным простым числам. Этот пример не является примером чрезвычайно полезного макроса, он всего лишь средство демонстрации вышеописанного процесса.

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

(defun primep (number)

(when (> number 1)

(loop for fac from 2 to (isqrt number) never (zerop (mod number fac)))))

(defun next-prime (number)

(loop for n from number when (primep n) return n))

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

(do-primes (p 0 19)

(format t "~d " p))

для выражения цикла, который выполняет тело для каждого простого числа, большего либо равного 0 и меньшего либо равного 19, используя переменную p для хранения очередного простого числа. Имеет смысл смоделировать этот макрос с помощью стандартных макросов DOLIST и DOTIMES ; макрос, следующий образцу существующих макросов, легче понять и использовать, нежели макросы, которые вводят неоправданно новый синтаксис.

Без использования макроса do-primes вы можете написать такой цикл путем использования DO (и двух вспомогательных функций, определенных ранее) следующим образом:

(do ((p (next-prime 0) (next-prime (1+ p))))

((> p 19))

(format t "~d " p))

Теперь вы готовы к написанию кода макроса, который будет выполнять необходимое преобразование.

<p>Макропараметры</p>

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

Но, кажется, такого подхода недостаточно для do-primes . Первый аргумент вызова do-primes является списком, содержащим имя переменной цикла, p ; нижнюю границу, 0; верхнюю границу, 19. Но, если вы посмотрите на раскрытие, список, как целое, не встречается в нем: эти три элемента разделены и вставлены в различные места.

Вы можете определить do-primes с двумя параметрами, первый для захвата этого списка и параметр &rest для захвата форм тела цикла, а затем разобрать первый список вручную подобным образом:

(defmacro do-primes (var-and-range &rest body)

(let ((var (first var-and-range))

(start (second var-and-range))

(end (third var-and-range)))

`(do ((,var (next-prime ,start) (next-prime (1+ ,var))))

((> ,var ,end))

,@body)))

Очень скоро я объясню как тело макроса генерирует правильное раскрытие; сейчас же вам следует отметить, что переменные var , start и end , каждая содержит значение, извлеченное из var-and-range , и эти значения затем подставляются в выражение квазицитирования, генерирующее раскрытие do-primes .

Однако, вам не нужно разбирать var-and-range вручную, так как список параметров макроса является так называемым списком деструктурируемых параметров. Деструктурирование, как и говорит название, осуществляет разбор некоторой структуры, в нашем случае списочной структуры форм, переданных макросу.

Внутри списка деструктурируемых параметров простое имя параметра может быть заменено вложенным списком параметров. Параметры в таком списке будут получать свои значения из элементов выражения, которое было бы связано с параметром, замененным этим списком. Например, вы можете заменить var-and-range списком (var start end) и три элемента списка будут автоматически деструктурированы в эти три параметра.

Другой особенностью списка параметров макросов является то, что вы можете использовать &body как синоним &rest . Семантически &body и &rest эквиваленты, но множество сред разработки будут использовать факт наличия параметра &body для изменения того, как они будут выравнивать код использования макроса, поэтому обычно параметры &body используются для захвата списка форм, которые составляют тело макроса.

Таким образом, вы можете улучшить определение макроса do-primes и дать подсказку как людям, читающим ваш код, так и вашим инструментам разработки, об его предназначении следующим образом

(defmacro do-primes ((var start end) &body body)

`(do ((,var (next-prime ,start) (next-prime (1+ ,var))))

((> ,var ,end))

,@body))

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

(do-primes var-and-range &rest body)

С новым же описанием она сможет указать вам, что вызов должен выглядеть следующим образом:

(do-primes (var start end) &body body)

Списки деструктурируемых параметров могут содержать параметры &optional , &key и &rest , а также вложенные деструктурируемые списки. Однако все эти возможности не нужны вам для написания do-primes .

<p>Генерация раскрытия</p>

Так как do-primes является довольно простым макросом, после деструктурирования аргументов все, что вам остается сделать это подставить их в шаблон для получения раскрытия.

Для простых макросов, наподобие do-primes , лучшим вариантом является использование специального синтаксиса квазицитирования. Коротко говоря, выражения квазицитирования подобны выражениям цитирования, за исключеним того, что вы можете "раскавычить" определенные подвыражения, предваряя их запятой, за которой возможно следует знак "at" (@). Без этого знака "at" запятая вызывает включение как есть значения следующего за ней подвыражения. Со знаком "at" значение, которое должно быть списком, "вклеивается" в окружающий список.

Другой пригодный способ думать о синтаксисе квазицитирования как об очень кратком способе написания кода, генерирующего списки. Такое представление о нем имеет преимущество, так как является очень близким к тому, что на самом деле происходит "под капотом": когда процедура чтения считывает выражение квазицитирования, она преобразует его в код, который генерирует соответствующую списковую структуру. Например, `(,a b) вероятно будет прочитано как (list a 'b). Стандарт языка не указывает, какой в точности код процедура чтения должна выдавать, пока она генерирует правильные списковые структуры.

Таблица 8-1 показывает некоторые примеры выражений квазицитирования вместе с эквивалентным создающим списки кодом, а также результаты, которые вы получите при вычислении как выражений квазицитирования, так и эквивалентного кода [101]:

Таблица 8-1. Примеры квазицитирования ^ Синтаксис квазицитирования Эквивалентый создающий списки код Результат | `(a (+ 1 2) c) (list 'a '(+ 1 2) 'c) (a (+ 1 2) c) | `(a ,(+ 1 2) c) (list 'a (+ 1 2) 'c) (a 3 c) | `(a (list 1 2) c) (list 'a '(list 1 2) 'c) (a (list 1 2) c) | `(a ,(list 1 2) c) (list 'a (list 1 2) 'c) (a (1 2) c) | `(a ,@(list 1 2) c) (append (list 'a) (list 1 2) (list 'c)) (a 1 2 c) |

Важно заметить, что нотация квазицитирования является просто удобством. Но это большое удобство. Для оценки того, насколько оно велико, сравните версию do-primes с квазицитированием со следующей версией, которая явно использует создающий списки код:

(defmacro do-primes-a ((var start end) &body body)

(append '(do)

(list (list (list var

(list 'next-prime start)

(list 'next-prime (list '1+ var)))))

(list (list (list '> var end)))

body))

Как вы очень скоро увидите, текущая реализация do-primes не обрабатывает корректно некоторые граничные случаи. Но первое, что вы должны проверить, это то, что она по крайней мере работает для исходного примера. Вы можете сделать это двумя способами. Во-первых, вы можете косвенно протестировать свою реализацию просто воспользовавшись ею (подразумевая, что если итоговое поведение корректно, то и раскрытие также корректно). Например, вы можете напечатать исходный пример использования do-primes в REPL и увидеть, что он и в самом деле напечатает правильную последовательность простых чисел.

CL-USER> (do-primes (p 0 19) (format t "~d " p))

2 3 5 7 11 13 17 19

NIL

Или же вы можете проверить макрос напрямую, посмотрев на раскрытие определенного вызова. Функция MACROEXPAND-1 получает любое выражение Lisp в качестве аргумента и возвращает результат осуществления одного шага раскрытия макроса [102]. Так как MACROEXPAND-1 является функцией, для дословной передачи ей формы макроса вы должны зацитировать эту форму. Теперь вы можете воспользоваться MACROEXPAND-1 для просмотра раскрытия предыдущего вызова [103].

CL-USER> (macroexpand-1 '(do-primes (p 0 19) (format t "~d " p)))

(DO ((P (NEXT-PRIME 0) (NEXT-PRIME (1+ P))))

((> P 19))

(FORMAT T "~d " P))

T

Также, для большего удобства, в SLIME вы можете проверить раскрытие макроса поместив курсор на открывающую скобку формы макроса в вашем исходном коде и набрав C-c RET для вызова функции Emacs slime-macroexpand-1 , которая передаст форму макроса в MACROEXPAND-1 и напечатает результат во временном буфере.

Теперь вы можете видеть, что результат раскрытия макроса совпадает с исходным написанным вручную раскрытием, и поэтому кажется, что do-primes работает.

<p>Устранение протечек</p>

В своем эссе "Закон дырявых абстракций" Джоэл Спольски придумал термин "дырявой абстракции" для описания такой абстракции, через которую "протекают" детали, абстрагирование от которых предполагается. Так как написание макроса это способ создания абстракции, вам следует убедиться, что ваш макрос излишне не "протекает" [104]

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

Текущее определение страдает от одной из трех возможных "протечек" макросов, а именно, оно вычисляет подформу end слишком много раз. Предположим, что вы вызвали do-primes с таким выражением, как (random 100) , на месте параметра end вместо использования числового литерала, такого, как 19.

(do-primes (p 0 (random 100))

(format t "~d " p))

Предполагаемым поведением здесь является итерирование по простым числам от нуля до какого-то случайного простого числа, возвращенного (random 100) . Однако, это не то, что делает текущая реализация, как это показывает MACROEXPAND-1 .

CL-USER> (macroexpand-1 '(do-primes (p 0 (random 100)) (format t "~d " p)))

(DO ((P (NEXT-PRIME 0) (NEXT-PRIME (1+ P))))

((> P (RANDOM 100)))

(FORMAT T "~d " P))

T

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

Это является "протечкой" абстракции, так как для корректного использования макроса, его пользователь должен быть осведомлен о том, что форма end будет вычислять более одного раза. Одним из способов устранения этой "протечки" является простое специфицирование ее как поведения do-primes . Но это не достаточно удовлетворительно: при реализации макросов вам следует пытаться соблюдать Правило Наименьшего Удивления. К тому же программисты обычно ожидают, что формы, которые они передают макросам, будут вычисляться не большее число раз, чем это действительно необходимо [105]. Более того, так как do-primes построена на основе модели стандартных макросов DOTIMES и DOLIST , которые вычисляют однократно все свои формы, кроме форм тела, то большинство программистов будут ожидать от do-primes подобного поведения.

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

(defmacro do-primes ((var start end) &body body)

`(do ((ending-value ,end)

(,var (next-prime ,start) (next-prime (1+ ,var))))

((> ,var ending-value))

,@body))

К сожалению данное исправление вводит две новые "протечки" в предоставляемую нашим макросом абстракцию.

Одна из этих "протечек" подобна проблеме множественных вычислений, которую мы только что исправили. Так как формы инициализации переменных цикла DO вычисляются в том порядке, в каком переменные определены, то когда раскрытие макроса вычисляется, выражение, переданное как end , будет вычислено перед выражением, переданным как start , то есть в обратном порядке от того, как они идут в вызове макроса. Эта "протечка" не вызывает никаких проблем пока start и end являются литералами вроде 0 и 19. Но, если они являются формами, которые могут иметь побочные эффекты, вычисление их в неправильном порядке снова нарушает Правило Наименьшего Удивления.

Эта "протечка" устраняется тривиально путем изменения порядка определения двух переменных.

(defmacro do-primes ((var start end) &body body)

`(do ((,var (next-prime ,start) (next-prime (1+ ,var)))

(ending-value ,end))

((> ,var ending-value))

,@body))

Последняя "протечка", которую нам нужно устранить, была создана использованием имени переменной end-value . Проблема заключается в том, что имя, которое должно быть полностью внутренней деталью реализации макроса, может вступить во взаимодействие с кодом, переданным макросу, или с контекстом, в котором макрос вызывается. Следующий, кажущийся вполне допустимым, вызов do-primes не работает корректно из-за данной "протечки":

(do-primes (ending-value 0 10)

(print ending-value))

То же касается и следующего вызова:

(let ((ending-value 0))

(do-primes (p 0 10)

(incf ending-value p))

ending-value)

И снова MACROEXPAND-1 может вам показать, в чем проблема. Первый вызов расширяется в следующее:

(do ((ending-value (next-prime 0) (next-prime (1+ ending-value)))

(ending-value 10))

((> ending-value ending-value))

(print ending-value))

Некоторые реализации Lisp могут отвергуть такой код из-за того, что ending-value используется дважды в качестве имен переменных одного и того-же цикла DO . Если же этого не произойдет, то код зациклится, так как ending-value никогда не станет больше себя самого.

Второй проблемный вызов расширяется следующим образом:

(let ((ending-value 0))

(do ((p (next-prime 0) (next-prime (1+ p)))

(ending-value 10))

((> p ending-value))

(incf ending-value p))

ending-value)

В этом случае сгенерированный код полностью допустим, но его поведение совсем не то, что нужно вам. Так как привязка ending-value, установленная с помощью LET снаружи цикла перекрывается переменной с таким же именем внутри DO , то форма (incf ending-value p) увеличивает переменную цикла ending-value вместо внешней переменной с таким же именем, создавая другой вечный цикл [106].

Очевидно, что то, что нам нужно для устранения этой "протечки" это символ, который никогда не будет использоваться снаружи кода, сгенерированного макросом. Вы можете попытать использовать действительно маловероятный символ, но это все равно не даст вам никаких гарантий. Вы можете также защитить себя в некоторой степени путем использования пакетов, описанных в главе 21. Но существует лучшее решение.

Функция GENSYM возвращает уникальный символ при каждом своем вызове. Такой символ никогда до этого не был прочитан процедурой чтения Lisp и, так как он не интернирован (isn't interned) ни в один пакет, никогда не будет прочитан ею. Поэтому, вместо использования литеральных имен наподобие ending-value , вы можете генерировать новый символ при каждом раскрытии do-primes .

(defmacro do-primes ((var start end) &body body)

(let ((ending-value-name (gensym)))

`(do ((,var (next-prime ,start) (next-prime (1+ ,var)))

(,ending-value-name ,end))

((> ,var ,ending-value-name))

,@body)))

Обратите внимание, что код, вызывающий GENSYM не является частью раскрытия; он запускается как часть процедуры раскрытия макроса и поэтому создает новый символ при каждом раскрытии макроса. Это может казаться несколько странным сначала: ending-value-name является переменной, чье значение является именем другой переменной. Но на самом деле тут нет никаких отличий от параметра var , чье значение также является именем переменной. Единственная разница состоит в том, что значение var было создано процедурой чтения, когда форма макроса была прочитана, а значение ending-value-name было сгенерированно программно при запуске кода макроса.

С таким определением две ранее проблемные формы расширяются в код, который работает так, как вам нужно. Первая форма:

(do-primes (ending-value 0 10)

(print ending-value))

расширяется в следующее:

(do ((ending-value (next-prime 0) (next-prime (1+ ending-value)))

(#:g2141 10))

((> ending-value #:g2141))

(print ending-value))

Теперь переменная, используемая для хранения конечного значения является сгенерированным функцией gensym символом, #:g2141. Имя идентификатора, G2141 , было сгенерировано с помощью GENSYM , но важно не это; важно то, что идентификатор хранит значение объекта. Сгенерированные таким образом символы печатаются в обычном синтаксисе для неинтернированных символов: с начальным #: .

Вторая ранее проблемная форма:

(let ((ending-value 0))

(do-primes (p 0 10)

(incf ending-value p))

ending-value)

после замены do-primes его раскрытием будет выглядеть подобным образом:

(let ((ending-value 0))

(do ((p (next-prime 0) (next-prime (1+ p)))

(#:g2140 10))

((> p #:g2140))

(incf ending-value p))

ending-value)

И снова, тут нет никакой "протечки", так как переменная ending-value , связанная окружающей цикл do-primes формой LET , больше не перекрывается никакими переменными, вводимыми в коде раскрытия.

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

Этим исправлением мы устранили все "протечки" в реализации do-primes . После получения некоторого опыта в написании макросов, вы научитесь писать макросы с заранее устраненными "протечками" такого рода. На самом деле это довольно просто, если вы будете следовать следующим правилам:

Если только нет определенной причины сделать иначе, включайте все подформы в раскрытие на такие позиции, чтобы они выполнялись в том же порядке, в каком они идут в вызове макроса. Если только нет определенной причины сделать иначе, убедитесь, что все подформы вычисляются лишь единожды, путем создания в раскрытии переменных для содержания значений вычисления форм аргументов и последующего использования этих переменных везде в раскрытии, где нужны значения этих форм. Используйте GENSYM во время раскрытия макросов для создания имен переменных, используемых в раскрытии.
<p>Макросы, создающие макросы</p>

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

На самом деле, вы уже видели один такой образец: многие макросы, как и последняя версия do-primes , начинаются с LET , который вводит несколько переменных, содержащих сгенерированные символы для использовании в раскрытии макроса. Так как это общий образец, почему бы нам не абстрагировать его с помощью его собственного макроса?

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

Предположим, вы хотите иметь возможность написать подобное:

(defmacro do-primes ((var start end) &body body)

(with-gensyms (ending-value-name)

`(do ((,var (next-prime ,start) (next-prime (1+ ,var)))

(,ending-value-name ,end))

((> ,var ,ending-value-name))

,@body)))

и получить do-primes , эквивалентный его предыдущей версии. Другими словами, with-gensyms должен раскрываться в LET , которая связывает каждую перечисленную переменную, ending-value-name в данном случае, со сгенерированным символом. Достаточно просто написать это с помощью простого шаблона-квазитирования.

(defmacro with-gensyms ((&rest names) &body body)

`(let ,(loop for n in names collect `(,n (gensym)))

,@body))

Обратите внимание, как мы можем использовать запятую для подстановки значения выражения LOOP . Этот цикл генерирует список связывающих форм, каждая из которых состоит из списка, содержащего одно из переданных with-gensyms имен, а также литеральный код (gensym) . Вы можете проверить, какой код сгенерирует выражение LOOP в REPL, заменив names списком символов.

CL-USER> (loop for n in '(a b c) collect `(,n (gensym)))

((A (GENSYM)) (B (GENSYM)) (C (GENSYM)))

После списка связывающих форм в качестве тела LET вклеивается аргумент body with-gensyms . Таким образом, из кода, который вы оборачиваете в with-gensyms , вы можете ссылаться на любое из имен переменных из списка переменных, переданного with-gensyms .

Если вы воспользуетесь macro-expand для формы with-gensyms в новом определении do-primes , то вы получите подобное:

(let ((ending-value-name (gensym)))

`(do ((,var (next-prime ,start) (next-prime (1+ ,var)))

(,ending-value-name ,end))

((> ,var ,ending-value-name))

,@body))

Выглядит неплохо. Хотя этот макрос довольно прост, очень важно ясно понимать то, когда различные макросы раскрываются: когда вы компилируете DEFMACRO do-primes , форма with-gensyms раскрывается в код, который вы только что видели. Таким образом, скомпилированная версия do-primes в точности такая же, как если бы вы написали внешний LET вручную. Когда вы компилируете функцию, которая использует do-primes , то для генерации расширения do-primes запускается код, сгенерированный with-gensyms , но сам with-gensyms при компиляции формы do-primes не нужен, так как он уже был раскрыт при компиляции do-primes .

<p>Другой классический макрос, создающий макросы: ONCE-ONLY</p>

Другим классическим макросом, создающим макросы, является once-only , который используется для генерации кода, вычисляющего определенные аргументы макроса только единожды и в определенном порядке. Используя once-only вы можете написать do-primes почти таким же простым способом, как исходную "протекающую" версию, следующим образом:

(defmacro do-primes ((var start end) &body body)

(once-only (start end)

`(do ((,var (next-prime ,start) (next-prime (1+ ,var))))

((> ,var ,end))

,@body)))

Однако, реализация once-only несколько запутанна для обычного пошагового объяснения, так как зависит от множества уровней квазицитирования и "раскавычивания". Если вы действительно хотите попрактиковаться в понимании макросов, вы можете попытаться разобраться, как он работает. Макрос выглядит следующим образом:

(defmacro once-only ((&rest names) &body body)

(let ((gensyms (loop for n in names collect (gensym))))

`(let (,@(loop for g in gensyms collect `(,g (gensym))))

`(let (,,@(loop for g in gensyms for n in names collect ``(,,g ,,n)))

,(let (,@(loop for n in names for g in gensyms collect `(,n ,g)))

,@body)))))

<p>Не только простые макросы</p>

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

9
<p>Практикум: Каркас для юнит-тестирования.</p>

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

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

Главная особенность автоматизированного тестирования состоит в том, что каркас отвечает за проверку, все ли тесты выполнились успешно. Вам не требуется тратить время на то, чтобы пробираться сквозь результаты, сверяя их с ожидаемыми компьютер может сделать это гораздо быстрее и аккуратнее вас. Как следствие, каждый тест должен быть выражением, которое вырабатывает логическое значение истина или ложь, тест выполнен успешно или провалился. К примеру, если вы тестируете встроенную функцию + , следующие выражения являются вполне разумными тестами [107]:

(= (+ 1 2) 3)

(= (+ 1 2 3) 6)

(= (+ -1 -3) -4)

Функции с побочными эффектами необходимо тестировать слегка по-другому вам придётся вызвать функцию и затем проверить наличие ожидаемых побочных эффектов [108]. Но в любом случае каждый тест сводится к логическому выражению: сработало или не сработало.

<p>Два первых подхода</p>

Если бы вы тестировали вручную, вы бы вводили эти выражения в REPL и проверяли бы, что они возвращают T . Но вам нужен каркас, который позволяет с лёгкостью организовывать и запускать эти тесты в любое время. Если вы хотите начать с самой простой работающей версии, вы можете просто написать функцию, которая вычисляет все тесты и возвращает T в случае успешного прохождения всех тестов (используя AND для этого).

(defun test-+ ()

(and

(= (+ 1 2) 3)

(= (+ 1 2 3) 6)

(= (+ -1 -3) -4)))

Для запуска тестов просто вызовите test-+ .

CL-USER> (test-+)

T

Пока функция возвращает T , вы знаете, что тесты проходят. Такой способ организации тестов также весьма выразителен вам не нужно писать много кода, обслуживающего тестирование. Однако при первом же проваливающемся тесте вы заметите, что отчёт о тестировании оставляет желать лучшего: если test-+ возвращает NIL , вы знаете, что какой-то тест провалился, но не имеете понятия, какой именно.

Давайте попробуем другой простой (можно даже сказать глупый) подход: чтобы проверить, что случилось с каждым тестом, будете писать так:

(defun test-+ ()

(format t "~:[FAIL~;pass~] ... ~a~%" (= (+ 1 2) 3) '(= (+ 1 2) 3))

(format t "~:[FAIL~;pass~] ... ~a~%" (= (+ 1 2 3) 6) '(= (+ 1 2 3) 6))

(format t "~:[FAIL~;pass~] ... ~a~%" (= (+ -1 -3) -4) '(= (+ -1 -3) -4)))

Теперь каждый тест будет сообщать результат отдельно. Часть ~:[FAIL~;pass~] форматной строки FORMAT печатает FAIL если первый аргумент ложен и pass если истинен [109] . Теперь запуск test-+ покажет подробности происходящего.

CL-USER> (test-+)

pass ... (= (+ 1 2) 3)

pass ... (= (+ 1 2 3) 6)

pass ... (= (+ -1 -3) -4)

NIL

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

Другая проблема состоит в том, что вы не получаете единого ответа, прошли ли все тесты успешно. Для трёх тестов достаточно легко проверить, что вывод не содержит строчек FAIL , но при наличии сотен тестов это начнёт надоедать.

<p>Рефакторинг</p>

Что вам действительно нужно это способ писать тесты так элегантно, как в первой функции test-+ , которая возвращает T или NIL , но также отчитывается о результатах индивидуальных тестов, так, как во второй версии. Поскольку вторая версия близка по фукнциональности к тому, что вам нужно, лучшее, что вы можете сделать проверить, можно ли исключить из неё раздражающее дублирование.

Простейший способ избавиться от повторяющихся похожих вызовов FORMAT создать новую функцию.

(defun report-result (result form)

(format t "~:[FAIL~;pass~] ... ~a~%" result form))

Теперь вы можете писать test-+ , вызывая report-result вместо FORMAT . Не слишком упрощает жизнь, но по крайней мере если вы решите изменить вид выдаваемых результатов, то вам придётся менять код только в одном месте.

(defun test-+ ()

(report-result (= (+ 1 2) 3) '(= (+ 1 2) 3))

(report-result (= (+ 1 2 3) 6) '(= (+ 1 2 3) 6))

(report-result (= (+ -1 -3) -4) '(= (+ -1 -3) -4)))

Следующее, что нужно сделать избавиться от дублирования тестового выражения, с присущим дублированию риском неправильной маркировки результата тестирования. Что вам нужно это возможность обработать тестовое выражение одновременно как код (для получения результата теста) и как данные (для использования в качестве метки теста). Использование кода, как данных это безошибочный признак того, что вам нужен макрос. Или, если посмотреть на это с другой стороны, вам нужен способ автоматизировать подверженное ошибкам написание вызовов report-result . Неплохо было бы написать что-то, похожее на

(check (= (+ 1 2) 3))

и чтобы это означало следующее:

(report-result (= (+ 1 2) 3) '(= (+ 1 2) 3))

Написание макроса для выполнения этой трансформации тривиально.

(defmacro check (form)

`(report-result ,form ',form))

Теперь вы можете изменить test-+ , чтобы использовать check .

(defun test-+ ()

(check (= (+ 1 2) 3))

(check (= (+ 1 2 3) 6))

(check (= (+ -1 -3) -4)))

Раз уж вы устраняете дублирование, почему бы не избавиться от повторяющихся вызовов check ? Можно заставить check принимать произвольное количество аргументов и заворачивать каждый из них в вызов report-result .

(defmacro check (&body forms)

`(progn

,@(loop for f in forms collect `(report-result ,f ',f))))

Это определение использует общепринятую идиому оборачивание набора форм в вызов PROGN , чтобы сделать их единой формой. Заметьте, как можно использовать ,@ для FIXME вклеивания результата выражения, которое возвращает список выражений, которые сами по себе созданы с помощью шаблона, созданного обратной кавычкой.

С новой версией check можно написать новую версию test-+ следующим образом:

(defun test-+ ()

(check

(= (+ 1 2) 3)

(= (+ 1 2 3) 6)

(= (+ -1 -3) -4)))

что эквивалентно следующему коду:

(defun test-+ ()

(progn

(report-result (= (+ 1 2) 3) '(= (+ 1 2) 3))

(report-result (= (+ 1 2 3) 6) '(= (+ 1 2 3) 6))

(report-result (= (+ -1 -3) -4) '(= (+ -1 -3) -4))))

Благодаря макросу check , этот вариант столь же краток, как первая версия test-+ , но раскрывается в код, который делает то же самое, что вторая версия. Кроме того, вы можете внести любые изменения в поведение test-+ , изменяя только check .

<p>Чиним возвращаемое значение</p>

Вы можете начать с изменения test-+ таким образом, чтобы его возвращаемое значение показывало, все ли тесты завершились успешно. Поскольку check отвечает за генерацию кода, который запускает тексты, вам нужно изменить его так, чтобы генерируемый код подсчитывал результаты тестов.

Для начала можно внести небольшое изменение в report-result , чтобы он возвращал результат выполняемого им теста.

(defun report-result (result form)

(format t "~:[FAIL~;pass~] ... ~a~%" result form)

result)

Теперь, когда report-result возвращает значение теста, кажется, что вы можете просто изменить PROGN на AND . К сожалению, AND не будет работать так, как вам хочется в этом случае, из-за своего FIXME short-circuit: как только один из тестов провалится, AND пропустит остальные. С другой стороны, если бы вы имели конструкцию, которая действует как AND , но не FIXME short-circuit, вы могли бы её использовать на месте PROGN . Common Lisp не предоставляет такой конструкции, но это не помешает вам использовать её: вы с легкостью написать макрос, предоставляющий такую конструкцию.

Оставляя тесты в стороне на минуту, вам нужен макрос (назовём его combine-results ), который позволит вам сказать

(combine-results

(foo)

(bar)

(baz))

и это будет значить

(let ((result t))

(unless (foo) (setf result nil))

(unless (bar) (setf result nil))

(unless (baz) (setf result nil))

result)

Единственный нетривиальный момент в написании этого макроса - это введение переменной ( result в предыдущем кусочке кода) в раскрытие макроса. Как вы видели в предыдущей главе, использование обычных имён для переменных в раскрытом макросе может заставить протекать абстракцию, так что вам нужно будет создать уникальное имя, что делается с помощью with-gensyms . Вы можете определить combine-results так:

(defmacro combine-results (&body forms)

(with-gensyms (result)

`(let ((,result t))

,@(loop for f in forms collect `(unless ,f (setf ,result nil)))

,result)))

Теперь вы можете исправить check , просто заменив PROGN на combine-results .

(defmacro check (&body forms)

`(combine-results

,@(loop for f in forms collect `(report-result ,f ',f))))

С этой версией check test-+ должен выдавать результаты своих трёх тестов и затем возвращать T , показывая, что все тесты завершились успешно [110].

CL-USER> (test-+)

pass ... (= (+ 1 2) 3)

pass ... (= (+ 1 2 3) 6)

pass ... (= (+ -1 -3) -4)

T

Если вы измените один из тестов так, чтобы он проваливался [111], возвращаемое значение изменится на NIL .

CL-USER> (test-+)

pass ... (= (+ 1 2) 3)

pass ... (= (+ 1 2 3) 6)

FAIL ... (= (+ -1 -3) -5)

NIL

<p>Улучшение отчёта</p>

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

(defun test-* ()

(check

(= (* 2 2) 4)

(= (* 3 5) 15)))

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

(defun test-arithmetic ()

(combine-results

(test-+)

(test-*)))

В этой функции вы используете combine-results вместо check , потому что и test-+ , и test-* сами позаботятся о выводе результатов своих тестов. Когда вы запустите test-arithmetic , вы получите следующий результат:

CL-USER> (test-arithmetic)

pass ... (= (+ 1 2) 3)

pass ... (= (+ 1 2 3) 6)

pass ... (= (+ -1 -3) -4)

pass ... (= (* 2 2) 4)

pass ... (= (* 3 5) 15)

T

Теперь представьте, что один из тестов провалился, и вам нужно найти проблему. Для пяти тестов и двух тестовых функций это будет не так сложно. Но представьте себе, что у вас 500 тестов, разнесённых по 20 функциям. Неплохо было бы, чтобы результаты сообщали вам, в какой функции находится каждый тест.

Поскольку код, который печатает результаты тестов, собран в report-result , вам нужен способ передать в неё информацию о том, в какой тестовой функции вы находитесь. Вы можете добавить параметр, сообщающий это, в report-result , но check , который генерирует вызовы report-result , не знает, из какой функции он вызван, что означает, что вам придётся изменить вызовы check , передавая аргумент, который он будет передавать дальше, в report-result .

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

Для начала определим переменную на верхнем уровне.

(defvar *test-name* nil)

Теперь слегка изменим report-result , чтобы включить *test-name* в вывод FORMAT .

(format t "~:[FAIL~;pass~] ... ~a: ~a~%" result *test-name* form)

После этих изменений тестовые функции всё ещё работают, но выдают следующие результаты из-за того, что *test-name* нигде не привязывается к значению, отличному от начального:

CL-USER> (test-arithmetic)

pass ... NIL: (= (+ 1 2) 3)

pass ... NIL: (= (+ 1 2 3) 6)

pass ... NIL: (= (+ -1 -3) -4)

pass ... NIL: (= (* 2 2) 4)

pass ... NIL: (= (* 3 5) 15)

T

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

(defun test-+ ()

(let ((*test-name* 'test-+))

(check

(= (+ 1 2) 3)

(= (+ 1 2 3) 6)

(= (+ -1 -3) -4))))

(defun test-* ()

(let ((*test-name* 'test-*))

(check

(= (* 2 2) 4)

(= (* 3 5) 15))))

Теперь результаты правильно помечены именами тестовых функций.

CL-USER> (test-arithmetic)

pass ... TEST-+: (= (+ 1 2) 3)

pass ... TEST-+: (= (+ 1 2 3) 6)

pass ... TEST-+: (= (+ -1 -3) -4)

pass ... TEST-*: (= (* 2 2) 4)

pass ... TEST-*: (= (* 3 5) 15)

T

<p>Выявление абстракций</p>

При внесении изменений в тестовые функции, вы снова получили дублирующийся код. Тестовые функции не только дважды включают своё имя первый раз при определении, второй раз при связывании с глобальной переменной *test-name* но обе они начинаются совершенно одинаково ( вся разница имя функции ). Вы могли бы попытаться избавиться от дублирования просто потому, что это некрасиво. Но если рассмотреть причину, вызвавшую дублирование, более подробно, то можно извлечь довольно важный урок по использованию макросов.

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

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

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

(defmacro deftest (name parameters &body body)

`(defun ,name ,parameters

(let ((*test-name* ',name))

,@body)))

Используя этот макрос, вы можете переписать test-+ следующим образом:

(deftest test-+ ()

(check

(= (+ 1 2) 3)

(= (+ 1 2 3) 6)

(= (+ -1 -3) -4)))

<p>Иерархия тестов</p>

Теперь, когда у вас есть полноценные тестовые функции, может возникнуть вопрос должна ли функция test-arithmetic также быть тестовой? Казалось бы если вы определите её с помощью deftest , то её связывание с *test-name* скроет связывания test-+ и test-* и это отразится на выводе результатов тестов.

Но представьте, что у вас есть тысяча ( или даже больше ) тестов, которые нужно как-то упорядочить. На самом нижнем уровне находятся такие функциями как test-+ и test-* , прямо выполняющих проверку. При наличии тысяч тестов их потребуется каким-либо образом упорядочить. Такие функции как test-arithmetic могут группировать схожие тестовые функции в наборы тестов. Допустим, что некоторые низкоуровневые тестовые функции могут использоваться в разных наборах тестов. Тогда вполне возможна такая ситуация, что тест будет пройден в одном контексте и провалится в другом. Если это случится, вам наверняка захочется узнать несколько больше, чем просто имя провалившегося теста.

Если вы определите test-arithmetic посредством deftest , сделав небольшие изменения при связывании с *test-name* , то сможете получить отчёты с более подробным описанием контекста выполнившегося теста:

pass ... (TEST-ARITHMETIC TEST-+): (= (+ 1 2) 3)

Поскольку процесс определения тестовых функций описан отдельным паттерном, изменить отчёт можно и не меняя код самих тестовых функций [112]. Сделать так, чтобы *test-name* хранил список имён тестовых функций вместо имени последней вызванной фнкции, очень просто. Вам нужно всего лишь изменить связывание:

(let ((*test-name* ',name))

на такое:

(let ((*test-name* (append *test-name* (list ',name))))

Так как APPEND возвращает новый список, составленный из его аргументов, это версия будет связывать *test-name* со списком, содержащим старое значение *test-name* , с новым именем, добавленным в конец списка [113]. После выхода из функции, старое значение *test-name* восстанавливается.

Теперь вы можете переопределить test-arithmetic используя deftest вместо DEFUN .

(deftest test-arithmetic ()

(combine-results

(test-+)

(test-*)))

В результате вы получите именно то, что хотели:

CL-USER> (test-arithmetic)

pass ... (TEST-ARITHMETIC TEST-+): (= (+ 1 2) 3)

pass ... (TEST-ARITHMETIC TEST-+): (= (+ 1 2 3) 6)

pass ... (TEST-ARITHMETIC TEST-+): (= (+ -1 -3) -4)

pass ... (TEST-ARITHMETIC TEST-*): (= (* 2 2) 4)

pass ... (TEST-ARITHMETIC TEST-*): (= (* 3 5) 15)

T

С ростом количества тестов, вы можете добавлять новые уровни и пока они будут определяться через deftest , вывод результата будт корректен. Так, если вы определите таким образом test-math :

(deftest test-math ()

(test-arithmetic))

то получите вот такой результат:

CL-USER> (test-math)

pass ... (TEST-MATH TEST-ARITHMETIC TEST-+): (= (+ 1 2) 3)

pass ... (TEST-MATH TEST-ARITHMETIC TEST-+): (= (+ 1 2 3) 6)

pass ... (TEST-MATH TEST-ARITHMETIC TEST-+): (= (+ -1 -3) -4)

pass ... (TEST-MATH TEST-ARITHMETIC TEST-*): (= (* 2 2) 4)

pass ... (TEST-MATH TEST-ARITHMETIC TEST-*): (= (* 3 5) 15)

T

<p>Подведение итогов</p>

Вы могли бы продолжить работу над этим каркасом, добавляя новые возможности но как каркас для написания тестов без особого напряжения и с возможностью использовать REPL , это очень неплохое начало. Ниже код приведён полностью, все 26 строк:

(defvar *test-name* nil)

(defmacro deftest (name parameters &body body)

"Define a test function. Within a test function we can call

other test functions or use 'check' to run individual test

cases."

`(defun ,name ,parameters

(let ((*test-name* (append *test-name* (list ',name))))

,@body)))

(defmacro check (&body forms)

"Run each expression in 'forms' as a test case."

`(combine-results

,@(loop for f in forms collect `(report-result ,f ',f))))

(defmacro combine-results (&body forms)

"Combine the results (as booleans) of evaluating 'forms' in order."

(with-gensyms (result)

`(let ((,result t))

,@(loop for f in forms collect `(unless ,f (setf ,result nil)))

,result)))

(defun report-result (result form)

"Report the results of a single test case. Called by 'check'."

(format t "~:[FAIL~;pass~] ... ~a: ~a~%" result *test-name* form)

result)

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

Вы начали с постановки задачи вычислить совокупность булевых выражений и узнать все ли они возвращают true . Простое AND работало и синтаксически было абсолютно верно но вывод результатов оставлял желать лучшего. Тогда вы написали немного дейстительно глуповатого кода, битком набитого повторениями и способствующими ошибкам выражениями, чтобы всё работало так, как вы хотели.

Естественно, что вы решили попробовать привести вторую версию программы к более ясному и красивому виду. Вы начали со стандартного приёма выделения части кода в отдельную функцию report-result . Увы, но использование report-result утомительно и чревато ошибками тестовое выражение приходится писать дважды. Тогда вы написали макрос check для автоматически корректного вызова report-result .

В процессе написания макроса check , вы добавили возможность оборачивать несколько вызовов report-result в один вызов check , сделав новую версию test-+ столь же краткой, как и первоначальную с AND .

Следующей задачей было исправить check таким образом, чтобы генерируемый этим макросом код возвращал t или nil в зависимости от того, все ли тесты прошли удачно. Прежде чем переиначивать check , вы предположили, что у вас есть FIXME non-short-circuit AND конструкция. В этом случае правка check тривиальное дело. Вы обнаружили, что хотя такой конструкции и нет, написать её самим совсем не трудно. После написания combine-results исправить check было элементарным делом.

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

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

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

10
<p>10. Числа, знаки и строки</p>

В то время как функции, переменные, макросы и 25 специальных операторов составляют основные блоки самого языка, строительными блоками ваших программ будут структуры данных, которые вы будете использовать. Как заметил Фред Брукс (Fred Brooks) в своей книге "Мифический человеко-месяц (The Mythical Man-Month)", "представление это сущность программирования." [114]

Common Lisp предоставляет поддержку для основных типов, обычно существующих в современных языках программирования: чисел (целых, с плавающей запятой и комплексных чисел), знаков (characters) [115], строк, массивов (включая многомерные массивы), списков, хеш-таблиц (hash tables), потоков ввода-вывода, и переносимое представление имен файлов. В Lisp функции также являются типом данных они могут быть сохранены в переменных, переданы как аргументы, использованы в качестве возвращаемых значений, а также созданы во время выполнения.

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

Однако сейчас вы начнете изучать встроенные типы данных. Поскольку Lisp является языком высокого уровня, детальная информация о реализации различных типов данных хорошо спрятана. С вашей точки зрения, как пользователя языка, встроенные типы данных определяются функциями, которые работают с ними. Так что, для изучения типа данных, вы просто должны изучить функции, которые используют этот тип. В дополнение к этому, большинство встроенных типов имеет специальный синтаксис, который понимает процедура чтения Lisp и который использует процедура печати Lisp. Поэтому, например, вы можете записывать строки как "foo" ; числа как 123 , 1/23 , и 1.23 ; а списки как (a b c) . Я буду описывать синтаксис для различных видов объектов тогда, когда я буду описывать функции для работы с этими типами данных.

В этой главе я опишу встроенные "скалярные" типы данных: числа, знаки и строки. С технической точки зрения, строки не я