Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
branch: master
Fetching contributors…

Cannot retrieve contributors at this time

1167 lines (965 sloc) 90.025 kb
\chapter{Практикум: простая база данных}
\label{ch:03}
\thispagestyle{empty}
Очевидно, перед тем как создавать настоящие программы на Lisp, вам необходимо изучить
язык. Но давайте смотреть правде в глаза~-- вы можете подумать <<Practical Common Lisp?
Не оксюморон ли это? Зачем тратить силы на изучение деталей языка, если на нем невозможно
сделать что-то дельное?>> Итак, для начала я приведу маленький пример того, что можно
сделать с помощью Common Lisp. В~этой главе вы напишете простую базу данных для
организации коллекции CD. В~главе~\ref{ch:27} вы будете использовать схожую технику при создании
базы данных записей в формате MP3 для вашего потокового MP3-сервера. Фактически можете
считать это частью вашего программного проекта~-- в конце концов, для того чтобы иметь
сколько-нибудь MP3-записей для прослушивания, совсем не помешает знать, какие записи у вас
есть, а какие нужно извлечь с диска.
В~этой главе я пройдусь по языку Lisp достаточно для того, чтобы вы продвинулись до
понимания того, каким образом работает код на нём. Но я не буду вдаваться в детали. Вы
можете не беспокоиться, если что-то здесь будет вам непонятно,~-- в нескольких следующих
главах все используемые здесь (а также многие другие) конструкции Common Lisp будут
описаны гораздо более систематически.
Одно замечание по терминологии: в этой главе я расскажу о некоторых операторах Lisp. В
главе~\ref{ch:04} вы узнаете, что Common Lisp предоставляет три разных типа операторов: функции,
макросы и операторы специального назначения. Для целей этой главы вам необязательно
понимать разницу. Однако я буду ссылаться на различные операторы как на функции, макросы
или специальные операторы, в зависимости от того, чем они на самом деле являются, вместо
того чтобы попытаться скрыть эти детали за одним словом~-- оператор. Сейчас вы можете
рассматривать функции, макросы и специальные операторы как более или менее эквивалентные
сущности\pclfootnote{Прежде чем я продолжу, очень важно, чтобы вы забыли все, что можете
знать о <<макросах>> в стиле \lstinline!#define!, реализованных в препроцессоре C. Макросы
Lisp не имеют с ними ничего общего.}.
Также имейте ввиду, что я не буду использовать все наиболее сложные техники Common Lisp
для вашей первой после <<Hello, world>> программы. Цель этой главы не в том, чтобы показать,
как вам следует писать базу данных на Lisp; скорее, цель в том, чтобы вы получили
представление, на что похоже программирование на Lisp, и видение того, что даже
относительно простая программа на Lisp может иметь много возможностей.
\section{CD и записи}
Чтобы отслеживать диски, которые нужно перекодировать в MP3, и знать, какие из них должны
быть перекодированы в первую очередь, каждая запись в базе данных будет содержать название
и имя исполнителя компакт-диска, оценку того, насколько он нравится пользователю, и флаг,
указывающий, был ли диск уже перекодирован. Итак, для начала вам необходим способ
представления одной записи в базе данных (другими словами, одного CD). Common Lisp
предоставляет для этого много различных структур данных, от простого четырёхэлементного
списка до определяемого пользователем с помощью CLOS класса данных.
Для начала вы можете остановиться на простом варианте и использовать список. Вы можете
создать его с помощью функции \lstinline{LIST}, которая, соответственно, возвращает
\textbf{список}\translationnote{\lstinline{LIST}~-- по английски \textbf{СПИСОК}. Кстати, последние
реализации Common Lisp позволяют писать и на родном для вас языке. Например, на русском
можно создать макрос \code{список}, который будет вызывать \lstinline{LIST}, например так:
\lstinline!(defmacro list (&body body) `(list ,@body))!.} из
переданных аргументов.
\begin{myverb}
CL-USER> (list 1 2 3)
(1 2 3)
\end{myverb}
Вы могли бы использовать четырёхэлементный список, отображающий позицию в списке на
соответствующее поле записи. Однако другая существующая разновидность списков, называемая
\textit{property list} (список свойств) или, сокращённо, \textit{plist}, в нашем случае
гораздо удобнее. \textit{Plist}~-- это такой список, в котором каждый нечётный элемент
является \textit{символом}, описывающим следующий (чётный) элемент списка. На этом этапе я
не буду углубляться в подробности понятия \textit{символ}; по своей природе это имя. Для
символов, именующих поля в базе данных, мы можем использовать частный случай символов,
называемый \textit{символами-ключами} (\textit{keyword symbol}). Ключ~-- это имя,
начинающееся с двоеточия (\lstinline{:}), например \lstinline{:foo}\translationnote{\lstinline{foo},
\lstinline{bar}~-- любимые имена переменных у англоговорящих программистов, пишущих книги и
документацию.}. Вот пример \textit{plist}, использующего символы-ключи \lstinline{:a},
\lstinline{:b} и \lstinline{:c} как имена свойств:
\begin{myverb}
CL-USER> (list :a 1 :b 2 :c 3)
(:A 1 :B 2 :C 3)
\end{myverb}
Заметьте, вы можете создать список свойств той же функцией \lstinline{LIST}, которой создавали
прочие списки. Характер содержимого~-- вот что делает его списком свойств.
Причина, по которой использование \textit{plist} является предпочтительным,~-- наличие
функции \lstinline{GETF}, в которую передают \textit{plist} и желаемый символ и получают
следующее за символом значение. Это делает \textit{plist} чем-то вроде упрощённой
хэш-таблицы. В~Lisp есть и <<настоящие>> хэш-таблицы, но для ваших текущих нужд достаточно
\textit{plist}, к тому же намного проще сохранять данные в такой форме в файл, это сильно
пригодится позже.
\begin{myverb}
CL-USER> (getf (list :a 1 :b 2 :c 3) :a)
1
CL-USER> (getf (list :a 1 :b 2 :c 3) :c)
3
\end{myverb}
Теперь, зная это, вам будет достаточно просто написать функцию \lstinline{make-cd}, которая
получит четыре поля в качестве аргументов и вернёт \textit{plist}, представляющий CD.
\begin{myverb}
(defun make-cd (title artist rating ripped)
(list :title title :artist artist :rating rating :ripped ripped))
\end{myverb}
Слово \lstinline{DEFUN} говорит нам\translationnote{Тем, для кого английский родной.}, что
эта запись определяет новую функцию. Имя функции~-- \lstinline{make-cd}. После имени следует
список параметров. Функция содержит четыре параметра~-- \lstinline{title}, \lstinline{artist},
\lstinline{rating} и \lstinline{ripped}. Всё, что следует за списком параметров,~-- тело функции. В
данном случае \textit{тело}~-- лишь форма, просто вызов функции \lstinline{LIST}. При вызове
\lstinline{make-сd} параметры, переданные при вызове, будут связаны с переменными в списке
параметров из объявления функции. Например, для создания записи о CD \textit{Roses} от
Kathy Mattea вы можете вызвать \lstinline{make-cd} примерно так:
\begin{myverb}
CL-USER> (make-cd "Roses" "Kathy Mattea" 7 t)
(:TITLE "Roses" :ARTIST "Kathy Mattea" :RATING 7 :RIPPED T)
\end{myverb}
\section{Заполнение CD}
Впрочем, создание одной записи~-- ещё не создание базы данных. Вам необходима более
комплексная структура данных для хранения записей. Опять же, простоты ради, список
представляется здесь вполне подходящим выбором. Также для простоты вы можете использовать
глобальную переменную \lstinline{*db*}, которую можно будет определить с помощью макроса
\lstinline{DEFVAR}. Звёздочки (*) в имени переменной~-- это договорённость, принятая в языке
Lisp при объявлении глобальных переменных\pclfootnote{Использование глобальной переменной
имеет ряд недостатков~-- например, в каждый момент времени вы можете работать только с
одной базой данных. В~главе~\ref{ch:27}, имея за плечами уже солидный багаж знаний о Lisp,
вы будете готовы к созданию более гибкой базы данных. В~главе~\ref{ch:06} вы также
увидите, что даже использование глобальных переменных в Common Lisp более гибко, чем это
возможно в других языках.}.
\begin{myverb}
(defvar *db* nil)
\end{myverb}
Для добавления элементов в \lstinline{*db*} можно использовать макрос \lstinline{PUSH}. Но
разумнее немного абстрагировать вещи и определить функцию \lstinline{add-record}, которая будет
добавлять записи в базу данных.
\begin{myverb}
(defun add-record (cd) (push cd *db*))
\end{myverb}
Теперь вы можете использовать \lstinline{add-record} вместе с \lstinline{make-cd} для добавления CD
в базу данных.
\begin{myverb}
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))
\end{myverb}
Всё, что REPL выводит после каждого вызова \lstinline{add-record}~-- значения, возвращаемые
последним выражением в теле функции, в нашем случае,~-- \lstinline{PUSH}. А \lstinline{PUSH}
возвращает новое значение изменяемой им переменной. Таким образом, после каждого
добавления порции данных вы видите содержимое вашей базы данных.
\section{Просмотр содержимого базы данных}
Вы также можете просмотреть текущее значение \lstinline{*db*} в любой момент,
набрав \lstinline{*db*} в REPL.
\begin{myverb}
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))
\end{myverb}
Правда, это не лучший способ просмотра данных. Вы можете написать функцию \lstinline{dump-db},
которая выводит содержимое базы данных в более читабельной форме, например так:
\begin{myverb}
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
\end{myverb}
Эта функция может выглядеть так:
\begin{myverb}
(defun dump-db ()
(dolist (cd *db*)
(format t "~{~a:~10t~a~%~}~%" cd)))
\end{myverb}
Работа функции заключается в циклическом обходе всех элементов \lstinline{*db*} с помощью макроса
\lstinline{DOLIST}, связывая на каждой итерации каждый элемент с переменной \lstinline{cd}. Для
вывода на экран каждого значения \lstinline{cd} используется функция \lstinline{FORMAT}.
Следует признать, вызов \lstinline{FORMAT} выглядит немного загадочно. Но в действительности
\lstinline{FORMAT} не особенно сложнее, чем функция \textit{printf} из С или
Perl или оператор \lstinline{%} из Python. В~главе~\ref{ch:18} я расскажу о
\lstinline{FORMAT} более подробно. Теперь же давайте шаг за шагом рассмотрим, как работает этот
вызов. Как было показано в главе~\ref{ch:02}, \lstinline{FORMAT} принимает по меньшей мере два аргумента,
первый из которых~-- поток, в который \lstinline{FORMAT} направляет свой вывод; \lstinline{t}~--
сокращённое обозначение потока \lstinline{*standard-output*}.
Второй аргумент \lstinline{FORMAT}~-- формат строки; он может содержать как символьный текст,
так и управляющие команды, контролирующие работу этой функции, например то, как она должна
интерпретировать остальные аргументы. Команды, управляющие форматом вывода, начинаются со
знака тильды (\lstinline!~!) (так же, как управляющие команды \textit{printf} начинаются с
\lstinline{%}). \lstinline{FORMAT} может принимать довольно много таких команд, каждую со
своим набором параметров\footnote{\begin{minipage}[t]{\linewidth}
Одна из наиболее <<классных>> управляющих команд
\lstinline{FORMAT}~-- команда \lstinline{~R}. Всегда хотели знать, как по-английски произносится
\textit{действительно большое} число? Lisp знает. Сделайте так:
\begin{myverb}
(format nil "~r" 1606938044258990275541962092)
\end{myverb}
\noindent{}и вы получите (разбито на строки для удобочитаемости): <<one octillion six hundred six
septillion nine hundred thirty-eight sextillion forty-four quintillion two hundred
fifty-eight quadrillion nine hundred ninety trillion two hundred seventy-five billion five
hundred forty-one million nine hundred sixty-two thousand ninety-two>>.
\end{minipage}}\hspace{\footnotenegspace}. Однако сейчас я
сфокусируюсь только на тех управляющих командах, которые необходимы для написания функции
\lstinline{dump-db}.
Команда \lstinline{~a} служит для придания выводимым строкам некоторой эстетичности. Она
принимает аргумент и возвращает его в удобочитаемой форме. Эта команда отобразит ключевые
слова без предваряющего двоеточия и строки~-- без кавычек. Например:
\begin{myverb}
CL-USER> (format t "~a" "Dixie Chicks")
Dixie Chicks
NIL
\end{myverb}
\noindent{}или:
\begin{myverb}
CL-USER> (format t "~a" :title)
TITLE
NIL
\end{myverb}
Команда \lstinline{~t} предназначена для табулирования. Например, \lstinline{~10t} указывает
\lstinline{FORMAT}, что необходимо выделить достаточно места для перемещения в десятый столбец
перед выполнением команды \lstinline{~a}. \lstinline{~t} не принимает аргументов.
\begin{myverb}
CL-USER> (format t "~a:~10t~a" :artist "Dixie Chicks")
ARTIST: Dixie Chicks
NIL
\end{myverb}
Теперь рассмотрим немного более сложные вещи. Когда \lstinline{FORMAT} обнаруживает
\lstinline!~{!, следующим аргументом должен быть список. \lstinline{FORMAT} циклично
просматривает весь список, на каждой итерации выполняя команды между \lstinline!~{! и
\lstinline!~}! и используя столько элементов списка, сколько нужно для вывода согласно
этим командам. В~функции \lstinline{dump-db} \lstinline{FORMAT} будет циклично просматривать
список и на каждой итерации принимать одно ключевое слово и одно значение
списка. Команда \lstinline!~%! не принимает аргументов, но заставляет \lstinline{FORMAT}
выполнять переход на новую строку. После выполнения команды \lstinline!~}! итерация
заканчивается, и последняя \lstinline!~%! заставляет \lstinline{FORMAT} сделать ещё один
переход на новую строку, чтобы записи, соответствующие каждому CD, были разделены.
Формально вы также можете использовать \lstinline{FORMAT} для вывода именно базы данных,
сократив тело функции \lstinline{dump-db} до одной строки.
\begin{myverb}
(defun dump-db ()
(format t "~{~{~a:~10t~a~%~}~%~}" *db*))
\end{myverb}
Это выглядит очень круто или очень страшно в зависимости от того, как посмотреть.
\section{Улучшение взаимодействия с пользователем}
Хотя функция \lstinline{add-record} прекрасно выполняет свои обязанности, она слишком необычна
для пользователя, незнакомого с Lisp. И если он захочет добавить в базу данных несколько
записей, это может показаться ему довольно неудобным. В~этом случае вы, возможно, захотите
написать функцию, которая будет запрашивать у пользователя информацию о нескольких~CD.
Тогда вам нужен какой-то способ запросить эту информацию у пользователя и считать её. Для
этого создадим следующую функцию:
\begin{myverb}
(defun prompt-read (prompt)
(format *query-io* "~a: " prompt)
(force-output *query-io*)
(read-line *query-io*))
\end{myverb}
Мы использовали уже знакомую нам функцию \lstinline{FORMAT}, чтобы вывести
приглашение. Заметим, что в строке, описывающей формат, отсутствует \lstinline{<<~%"},
поэтому перевода курсора на новую строку не происходит. Вызов \lstinline{FORCE-OUTPUT}
необходим в некоторых реализациях для уверенности в том, что Lisp не будет ожидать вывода
новой строки перед выводом приглашения.
Теперь прочитаем одну строку текста с помощью (очень удачно названной!) функции
\lstinline{READ-LINE}. Переменная \lstinline{*query-io*} является глобальной (о чем можно
догадаться по наличию в её имени символов \lstinline{*}), она содержит входной поток, связанный
с терминалом. Значение, возвращаемое функцией \lstinline{prompt-read},~-- это значение
последней её формы, вызова \lstinline{READ-LINE}, возвращающего прочитанную им строку (без
завершающего символа новой строки).
Вы можете скомбинировать уже существующую функцию \lstinline{make-cd} с \lstinline{prompt-read},
чтобы построить функцию создания новой записи о CD из данных, которые \lstinline{make-cd} по
очереди получает для каждого значения.
\begin{myverb}
(defun prompt-for-cd ()
(make-cd
(prompt-read "Title")
(prompt-read "Artist")
(prompt-read "Rating")
(prompt-read "Ripped [y/n]")))
\end{myverb}
Это почти правильно, если не считать того, что функция \lstinline{prompt-read} возвращает
строку. Это хорошо подходит для полей Title и Artist, но значения полей Rating и Ripped
--- числовое и булево. В~зависимости от того, насколько развитым вы хотите сделать
пользовательский интерфейс, можете проверять подстроки произвольной длины, чтобы
удостовериться в корректности введённых пользователем данных. Теперь давайте опробуем
самый очевидный (хотя и не лучший) вариант: мы можем упаковать вызов \lstinline{prompt-read},
запрашивающий у пользователя его оценку диска, в вызов специфичной для Lisp функции
\lstinline{PARSE-INTEGER}. Это можно сделать так:
\begin{myverb}
(parse-integer (prompt-read "Rating"))
\end{myverb}
К сожалению, по умолчанию функция \lstinline{PARSE-INTEGER} сообщает об ошибке, если ей не
удаётся разобрать число из введённой строки или если в строке присутствует <<нечисловой>>
мусор. Однако она может принимать дополнительный параметр \lstinline{:junk-allowed}, который позволит
нам ненадолго расслабиться.
\begin{myverb}
(parse-integer (prompt-read "Rating") :junk-allowed t)
\end{myverb}
Остаётся ещё одна проблема~-- если \lstinline{PARSE-INTEGER} не удастся выделить число среди
<<мусорных>> данных, она вернёт не число, а \lstinline{NIL}. Следуя нашему подходу <<сделать
просто, пусть даже не совсем правильно>>, мы в этом случае можем просто задать 0 и
продолжить. Макрос \lstinline{OR} здесь~-- как раз то, что нужно. Это то же самое, что и
операция \lstinline{||} в Perl, Python, Java и C. Макрос принимает набор выражений и
вычисляет их по одному, слева направо. Если какое-нибудь из них даёт истинное значение, то
оно возвращается как результат макроса \lstinline{OR}, а остальные не вычисляются. Если
все выражения оказываются ложными, тогда макрос \lstinline{OR} возвращает ложь
(\lstinline{NIL}). Таким образом, используем следующую запись:
\begin{myverb}
(or (parse-integer (prompt-read "Rating") :junk-allowed t) 0)
\end{myverb}
\noindent{}чтобы получить 0 в качестве значения по умолчанию.
Исправление кода для запроса состояния Ripped немного проще. Можно воспользоваться
стандартной функцией Common Lisp \lstinline{Y-OR-N-P}.
\begin{myverb}
(y-or-n-p "Ripped [y/n]: ")
\end{myverb}
Фактически этот вызов является самой отказоустойчивой частью \lstinline{prompt-for-cd},
поскольку \lstinline{Y-OR-N-P} будет повторно запрашивать у пользователя состояние флага
Ripped, если он введёт что-нибудь, начинающееся не с \textit{y}, \textit{Y}, \textit{n}
или \textit{N}.
Собрав код вместе, получим достаточно надёжную функцию \lstinline{prompt-for-cd}:
\begin{myverb}
(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]: ")))
\end{myverb}
Наконец, мы можем закончить интерфейс добавления CD, упаковав \lstinline{prompt-for-cd} в
функцию, циклично запрашивающую пользователя о новых данных. Воспользуемся простой формой
макроса \lstinline{LOOP}, выполняющего выражения в своём теле до тех пор, пока его выполнение
не будет прервано вызовом \lstinline{RETURN}. Например:
\begin{myverb}
(defun add-cds ()
(loop (add-record (prompt-for-cd))
(if (not (y-or-n-p "Another? [y/n]: ")) (return))))
\end{myverb}
Теперь с помощью \lstinline{add-cds} добавим в базу несколько новых дисков.
\begin{myverb}
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
\end{myverb}
\section{Сохранение и загрузка базы данных}
Хорошо иметь удобный способ добавления записей в базу данных. Но пользователю вряд ли
понравится заново добавлять все записи после каждого перезапуска Lisp. К счастью,
используя текущие структуры данных, применяемые для представления информации, сохранить
данные в файл и загрузить их позже~-- задача тривиальная. Далее приводится функция
\lstinline{save-db}, которая принимает в качестве параметра имя файла и сохраняет в него
текущее состояние базы данных:
\begin{myverb}
(defun save-db (filename)
(with-open-file (out filename
:direction :output
:if-exists :supersede)
(with-standard-io-syntax
(print *db* out))))
\end{myverb}
Макрос \lstinline{WITH-OPEN-FILE} открывает файл, связывает поток с переменной, выполняет
набор инструкций и затем закрывает файл. Он также гарантирует, что файл обязательно
закроется, даже если во время выполнения тела макроса что-то пойдёт не так. Список,
находящийся сразу после \lstinline{WITH-OPEN-FILE}, является не вызовом функции, а частью
синтаксиса, определяемого этим макросом. Он содержит имя переменной, хранящей файловый
поток, в который в теле макроса \lstinline{WITH-OPEN-FILE} будет вестись запись, значение,
которое должно быть именем файла, и несколько параметров, управляющих режимом открытия
файла. В~нашем примере файл будет открыт для записи (задаётся параметром \lstinline{:direction}
\lstinline{:output}), и, если файл с таким именем уже существует, его содержимое будет
перезаписано (параметр \lstinline{:if-exists} \lstinline{:supersede}).
После того как файл открыт, всё, что вам нужно,~-- это печать содержимого базы данных с
помощью \lstinline{(print *db* out)}. В~отличие от \lstinline{FORMAT}, функция \lstinline{PRINT}
печатает объекты Lisp в форме, которую Lisp может прочитать. Макрос
\lstinline{WITH-STANDARD-IO-SYNTAX} гарантирует, что переменным, влияющим на поведение
функции \lstinline{PRINT}, присвоены стандартные значения. Используйте этот же макрос и при
чтении данных из файла для гарантии совместимости операций записи и чтения.
Аргументом функции \lstinline{save-db} должна являться строка, содержащая имя файла, в который
пользователь хочет сохранить базу данных. Точный формат строки зависит от используемой
операционной системы. Например, в Unix пользователь может вызвать функцию \lstinline{save-db}
таким образом:
\begin{myverb}
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))
\end{myverb}
В~Windows имя файла может выглядеть так: \lstinline!c:/my-cds.db!. Или так:
\lstinline!c:\\my-cds.db!\pclfootnote{На самом деле Windows позволяет использовать косую
черту (<<прямые слэши>>) в именах файлов, хотя в качестве разделителей имён директорий в
этой ОС обычно используются обратные косые (<<обратные слэши>>). Это очень удобно, ведь
иначе вам каждый раз приходилось бы дублировать обратную косую черту, так как это~--
Escape-символ в строках Lisp.}.
Вы можете открыть этот файл в любом текстовом редакторе и посмотреть, как выглядят
записи. Вы должны увидеть что-то очень похожее на вывод REPL, если наберёте \lstinline{*db*}.
Функция загрузки базы данных из файла реализуется аналогично:
\begin{myverb}
(defun load-db (filename)
(with-open-file (in filename)
(with-standard-io-syntax
(setf *db* (read in)))))
\end{myverb}
В~этот раз нет необходимости задавать \lstinline{:direction} в параметрах
\lstinline{WITH-OPEN-FILE}, так как её значение по умолчанию~-- \lstinline{:input}. И вместо
печати вы используете функцию \lstinline{READ} для чтения из потока \lstinline{in}. Это та же
процедура чтения, что и в REPL, и он может прочитать любое выражение на Lisp, которое
можно написать в строке приглашения REPL. Однако в нашем случае вы просто читаете и
сохраняете выражение, не выполняя его. И снова макрос \lstinline{WITH-STANDARD-IO-SYNTAX}
гарантирует, что \lstinline{READ} использует тот же базовый синтаксис, что и функция
\lstinline{save-db}, когда она печатает данные с помощью \lstinline{PRINT}.
Макрос \lstinline{SETF} является главным оператором присваивания в Common Lisp. Он
присваивает своему первому аргументу результат вычисления второго аргумента. Таким образом,
в \lstinline{load-db} переменная \lstinline{*db*} будет содержать объект, прочитанный из файла, а
именно список списков, записанных функцией \lstinline{save-db}. Обратите внимание на то, что
\lstinline{load-db} затирает то, что было в \lstinline{*db*} до её вызова. Так что если вы добавили
записи, используя \lstinline{add-records} или \lstinline{add-cds}, и не сохранили их функцией
\lstinline{save-db}, эти записи будут потеряны.
\section{Выполнение запросов к базе данных}
Теперь, когда у вас есть способ сохранения и загрузки базы данных вместе с удобным
интерфейсом для добавления новых записей, ваша коллекция в скором времени может разрастись
до такого размера, что вы уже не захотите распечатывать всю базу данных лишь для того,
чтобы просмотреть её содержимое. Вам нужно как-то выполнять запросы к базе
данных. Возможно, что вы предпочли бы написать что-нибудь вроде:
\begin{myverb}
(select :artist "Dixie Chicks")
\end{myverb}
\noindent{}и в ответ на этот запрос получить список всех записей исполнителя Dixie Chicks. И снова
оказалось, что выбор списка в качестве контейнера данных был очень удачным.
Функция \lstinline{REMOVE-IF-NOT} принимает предикат и список в качестве параметров и
возвращает список, содержащий только элементы исходного списка, удовлетворяющие
предикату. Другими словами, она удаляет все элементы, не удовлетворяющие предикату. На
самом деле \lstinline{REMOVE-IF-NOT} ничего не удаляет~-- она создаёт новый список, оставляя
исходный список нетронутым. Эта операция аналогична работе утилиты grep. Предикатом может
быть любая функция, принимающая один аргумент и возвращающая логическое значение~--
\lstinline{NIL} (ложь) или любое другое значение (истина).
Например, если вы хотите получить все чётные элементы из списка чисел, можете использовать
\lstinline{REMOVE-IF-NOT} таким образом:
\begin{myverb}
CL-USER> (remove-if-not #'evenp '(1 2 3 4 5 6 7 8 9 10))
(2 4 6 8 10)
\end{myverb}
В~этом случае предикатом является функция \lstinline{EVENP}, которая возвращает <<истину>>,
если её аргумент~-- чётное число. Нотация \lstinline!#'! является сокращением выражения
<<Получить функцию с данным именем>>. Без \lstinline!#'! Lisp обратится к \lstinline{EVENP} как к
имени переменной и попытается получить её значение, а не саму функцию.
Вы также можете передать в \lstinline{REMOVE-IF-NOT} анонимную функцию. Например, если бы
\lstinline{EVENP} не существовало, вы могли бы так написать предыдущее выражение:
\begin{myverb}
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)
\end{myverb}
В~этом случае предикатом является анонимная функция
\begin{myverb}
(lambda (x) (= 0 (mod x 2)))
\end{myverb}
\noindent{}которая проверяет, равен ли нулю остаток от деления аргумента на 2 (другими словами,
является ли аргумент чётным). Если вы хотите извлечь только нечётные числа, используя
анонимную функцию, вы можете написать следующее:
\begin{myverb}
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)
\end{myverb}
Заметьте, что \lstinline{lambda} не является именем функции,~-- это слово показывает, что вы
определяете анонимную функцию\pclfootnote{Слово \lstinline{lambda} используется в Lisp из-за его
изначальной связи с лямбда-исчислением, математическим формализмом, изобретённым для
изучения математических функций.}. Если не считать имени, \lstinline{LAMBDA}-выражение
выглядит очень похожим на \lstinline{DEFUN}: после слова \lstinline{lambda} следует список
параметров, за которым идёт тело функции.
Чтобы выбрать все альбомы Dixie Chicks из базы данных, используя
\lstinline{REMOVE-IF-NOT}, вам нужна функция, возвращающая <<истину>>, если поле в записи
\lstinline{artist} содержит значение <<Dixie Chicks>>. Помните, мы выбрали \textit{список
свойств} в качестве представления записей базы данных, потому что функция
\lstinline{GETF} может извлекать из \textit{списка свойств} именованные поля. Итак,
полагая, что \lstinline{cd} является именем переменной, хранящей одну запись базы данных,
вы можете использовать выражение \lstinline{(getf cd :artist)}, чтобы извлечь имя
исполнителя. Функция \lstinline{EQUAL} посимвольно сравнивает переданные ей строковые
параметры. Таким образом, \lstinline{(equal (getf cd :artist) "Dixie Chicks")} проверит,
хранит ли поле \lstinline{artist}, для текущей записи в переменной \lstinline{cd},
значение <<Dixie Chicks>>. Всё, что вам нужно,~-- упаковать это выражение в
\lstinline{LAMBDA}-форму, чтобы создать анонимную функцию и передать её
\lstinline{REMOVE-IF-NOT}.
\begin{myverb}
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))
\end{myverb}
Предположим теперь, что вы хотите упаковать всё выражение в функцию, которая принимает имя
исполнителя в качестве параметра. Вы можете записать это так:
\begin{myverb}
(defun select-by-artist (artist)
(remove-if-not
#'(lambda (cd) (equal (getf cd :artist) artist))
*db*))
\end{myverb}
Заметьте, что анонимная функция, содержит код который не будет выполнен, пока функция не
вызвана в \lstinline{REMOVE-IF-NOT}, тем не менее она может ссылаться на переменную
\lstinline{artist}. В~этом случае анонимная функция не просто избавляет вас от необходимости
писать обычную функцию,~-- она позволяет вам написать функцию, которая берёт часть своего
значения~-- содержимое поля \lstinline{artist}~-- из контекста, в котором она вызывается.
Итак, мы покончили с функцией \lstinline{select-by-artist}. Однако выборка по исполнителю~--
лишь одна разновидность запросов, которые вам захочется реализовать. Вы можете написать
ещё несколько функций, таких как \lstinline{select-by-title}, \lstinline{select-by-rating},
\lstinline{select-by-title-and-artist}, и так далее. Но все они будут идентичными, за
исключением содержимого анонимной функции. Вместо этого вы можете создать более
универсальную функцию \lstinline{select}, которая принимает функцию в качестве аргумента.
\begin{myverb}
(defun select (selector-fn)
(remove-if-not selector-fn *db*))
\end{myverb}
А что случилось с \lstinline!#'!? Дело в том, что в этом случае вам не нужно, чтобы функция
\lstinline{REMOVE-IF-NOT} использовала функцию под названием \lstinline{selector-fn}. Вы хотите,
чтобы она использовала анонимную функцию, переданную в качестве аргумента функции
\lstinline{select} в переменной \lstinline{selector-fn}. Однако символ \lstinline!#'! вернулся в вызов
\lstinline{select}:
\begin{myverb}
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))
\end{myverb}
Правда, это выглядит довольно грубо. К счастью, вы можете упаковать создание анонимной функции.
\begin{myverb}
(defun artist-selector (artist)
#'(lambda (cd) (equal (getf cd :artist) artist)))
\end{myverb}
\lstinline{artist-selector} возвращает функцию, имеющую ссылку на переменную, которая
перестанет существовать после выхода из \lstinline{artist-selector}\pclfootnote{Техническое
обозначение функции, ссылающейся на свободную переменную в своём контексте,~--
\textbf{замыкание}, потому что функция как бы <<смыкается>> над переменной. Я подробнее
расскажу о замыканиях в главе~\ref{ch:06}.}. Функция выглядит странно, но она работает
именно так, как нам нужно: если вызвать \lstinline{artist-selector} с аргументом <<Dixie
Chicks>>, мы получим анонимную функцию, которая ищет CD с полем \lstinline{:artist},
содержащим \lstinline{"Dixie Chicks"}, и если вызвать её с аргументом
\lstinline{"Lyle Lovett"}, то мы получим другую функцию, которая будет искать CD с полем
\lstinline{:artist}, содержащим \lstinline{"Lyle Lovett"}. Итак, мы можем переписать вызов
\lstinline{select} следующим образом:
\begin{myverb}
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))
\end{myverb}
Теперь нам понадобится больше функций, чтобы генерировать выражения для выбора. Но так как
вы не хотите писать \lstinline{select-by-title}, \lstinline{select-by-rating} и др., потому что они
будут во многом схожими, вы не станете создавать множество почти идентичных генераторов
выражений для выбора значений для каждого из полей. Почему бы не написать генератор
функции-выражения для выбора общего назначения~-- функцию которая, в зависимости от
передаваемых ей аргументов будет генерировать выражение выбора для разных полей или,
может быть, даже комбинации полей? Вы можете напи\-сать такую функцию, но сначала нам
придётся пройти краткий курс для овладения средством, называемым
\textit{параметрами-ключами} (keyword parameters).
В~функциях, что вы писали до этого, вы задавали простой список параметров, которые
связывались с соответствующими аргументами в вызове функции. Например, следующая функция:
\begin{myverb}
(defun foo (a b c) (list a b c))
\end{myverb}
\noindent{}имеет три параметра: \lstinline{a}, \lstinline{b} и \lstinline{c},~-- и должна быть вызвана с тремя
аргументами. Но иногда возникает необходимость в вызове функции, которая может вызываться
с переменным числом аргументов. Параметры-ключи~-- один из способов это сделать. Версия
\lstinline{foo} с использованием параметров-ключей может выглядеть так:
\begin{myverb}
(defun foo (&key a b c) (list a b c))
\end{myverb}
Единственное отличие~-- элемент \lstinline!&key! в начале списка аргументов. Однако вызовы
новой функции \lstinline{foo} выглядят немного по-другому. Все нижеперечисленные варианты
вызова \lstinline{foo} допустимы, результат вызова помещён справа от \lstinline!==>!.
\begin{myverb}
(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)
\end{myverb}
Как показывают эти примеры, значения переменных \lstinline{a}, \lstinline{b} и \lstinline{c} привязаны
к значениям, которые следуют за соответствующими ключевыми словами. И если какой-либо
ключ в вызове отсутствует, соответствующая переменная устанавливается в \lstinline{NIL}. Я не
буду уточнять, как именно задаются ключевые параметры и как они соотносятся с другими
типами параметров, но вам важно знать одну деталь.
Обычно, когда функция вызывается без аргумента для конкретного параметра-ключа, параметр
будет иметь значение \lstinline{NIL}. Но иногда нужно различать \lstinline{NIL}, который был явно
передан в качестве аргумента к параметру-ключу, и \lstinline{NIL}, который задаётся по
умолчанию. Чтобы сделать это, при задании параметра-ключа вы можете заменить обычное имя
списком, состоящим из имени параметра, его значения по умолчанию и другого имени
параметра, называемого параметром \lstinline{supplied-p}. Этот параметр \lstinline{supplied-p} будет
содержать значения <<истина>> или <<ложь>> в зависимости от того, действительно ли для
данного параметра-ключа в данном вызове функции был передан аргумент. Вот версия новой
функции \lstinline{foo}, которая использует эту возможность.
\begin{myverb}
(defun foo (&key a (b 20) (c 30 c-p)) (list a b c c-p))
\end{myverb}
Результаты тех же вызовов теперь выглядят иначе:
\begin{myverb}
(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)
\end{myverb}
Основной генератор выражения выбора, который можно назвать \lstinline{where}, является
функцией, принимающей четыре параметра-ключа для соответствующих полей в наших записях CD
и генерирующей выражение выбора, которое возвращает все записи о CD, совпадающие со
значениями, задаваемыми в \lstinline{where}. Например, можно будет написать такое выражение:
\begin{myverb}
(select (where :artist "Dixie Chicks"))
\end{myverb}
\noindent{}или такое:
\begin{myverb}
(select (where :rating 10 :ripped nil))
\end{myverb}
Функция выглядит так:
\begin{myverb}
(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))))
\end{myverb}
Эта функция возвращает анонимную функцию, возвращающую логическое И для одного условия в
каждом поле записей о CD. Каждое условие проверяет, задан ли подходящий аргумент, и если
задан, то сравнивает его значение со значением соответствующего поля в записи о CD, или
возвращает \lstinline{t}, обозначение истины в Lisp, если аргумент не был задан. Таким образом,
выражение выбора возвратит \lstinline{t} только для тех CD, описание которых совпало по
значению с аргументами, переданными \lstinline{where}\footnote{Заметьте, что в Lisp оператор
\lstinline{IF}, как и всё остальное, является выражением, возвращающим значение. Вообще, он
больше напоминает тернарный оператор \lstinline{(?:)} в Perl, Java и C, поскольку вполне
допустимо такое выражение в этих языках:
\begin{myverb}
some_var = some_boolean ? value1 : value2;
\end{myverb}
\noindent{}А такое~-- нет:
\begin{myverb}
some_var = if (some_boolean) value1; else value2;
\end{myverb}
\noindent{}так как в этих языках \lstinline{if}~-- просто оператор, а не выражение.}\hspace{\footnotenegspace}. Заметьте, что,
чтобы задать ключ-параметр \lstinline{ripped}, вам необходимо использовать список из трёх
элементов, потому что вам нужно знать, действительно ли вызывающая функция передала
ключ-параметр \lstinline{:ripped nil}, означающий <<Выбрать те CD, в поле \lstinline{ripped} которых
установлено значение \lstinline{NIL}>>, либо опустила его, что означает <<Мне всё равно, какое
значение установлено в поле \lstinline{ripped}>>.
\section{Обновление существующих записей — повторное использование where}
Теперь, после того как у вас есть достаточно универсальные функции \lstinline{select} и
\lstinline{where}, очень логичной представляется реализация следующей возможности, которая
необходима каждой базе данных,~-- возможности обновления отдельных записей. В~SQL команда
\lstinline{update} используется для обновления набора записей, удовлетворяющих конкретному
условию \lstinline{where}. Эта модель кажется хорошей, особенно когда у вас уже есть генератор
условий \lstinline{where}. Фактически функция \lstinline{update}~-- применение некоторых идей,
которые вы уже видели: использование передаваемого выражения выбора для указания записей,
подлежащих обновлению, и использование аргументов-ключей для задания нового
значения. Новая вещь здесь~-- использование функции \lstinline{MAPCAR}, которая проходит по
списку, в нашем случае это \lstinline{*db*}, и возвращает новый список, содержащий результаты
вызова функции для каждого элемента исходного списка.
\begin{myverb}
(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*)))
\end{myverb}
Ещё одна новинка в этой функции\translationnote{Автор ещё забыл упомянуть макрос \lstinline{WHEN}. Он
будет подробно рассмотрен в главе~\ref{ch:07}.}~-- приложение \lstinline{SETF} к
сложной форме вида \lstinline{(getf row :title)}. Я расскажу о \lstinline{SETF} подробнее в главе~\ref{ch:06},
но сейчас вам просто нужно знать, что это общий оператор присваивания, который может
использоваться для присваивания друг другу различных вещей, а не только переменных. (То,
что \lstinline{SETF} и \lstinline{GETF} имеют настолько похожие имена,~-- просто совпадение. Между
ними нет никакой особой взаимосвязи.) Сейчас достаточно знать, что после выполнения
\lstinline{(setf (getf row :title) title)} у списка свойств, на который ссылается \lstinline{row},
значением переменной, следующей за именем свойства \lstinline{:title}, будет title. С помощью
функции \lstinline{update}, если вы решите, что действительно любите творчество Dixie Chicks и
что все их альбомы должны быть оценены в 11 баллов, можете выполнить следующую
форму\translationnote{Странно, что функция \lstinline{update} здесь возвращает \lstinline{NIL}, ведь последней
операцией является \lstinline{SETF *db* value}, которая, в свою очередь, возвращает
присвоенное значение. То есть после присваивания можно наблюдать изменения (sbcl-1.03).}:
\begin{myverb}
CL-USER> (update (where :artist "Dixie Chicks") :rating 11)
NIL
\end{myverb}
Результат работы функции будет выглядеть так:
\begin{myverb}
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))
\end{myverb}
Добавить функцию удаления строк из базы данных ещё проще.
\begin{myverb}
(defun delete-rows (selector-fn)
(setf *db* (remove-if selector-fn *db*)))
\end{myverb}
Функция \lstinline{REMOVE-IF} является дополнением к \lstinline{REMOVE-IF-NOT}, она возвращает
список всех элементов, удалив те из них, что удовлетворяют предикату. Так же, как и
\lstinline{REMOVE-IF-NOT}, она в действительности не изменяет список, который был ей передан
в качестве параметра, тем не менее, сохраняя результат обратно в \lstinline{*db*},
\lstinline{delete-rows}\pclfootnote{Вы должны использовать имя \lstinline{delete-rows} вместо более
очевидного \lstinline{delete}, потому что в Common Lisp уже есть функция
\lstinline{DELETE}. Система пакетов Lisp предоставляет возможность разрешать такие
конфликты имён, так что, если хотите, можете иметь в своей программе собственную функцию
\lstinline{delete}. Но сейчас ещё рано рассказывать вам о пакетах.}, фактически изменяет
содержимое базы данных\pclfootnote{Если вы беспокоитесь о том, что в этом коде могут
возникнуть утечки памяти, будьте уверены: Lisp был первым языком, в котором появилась
сборка мусора (и, раз уж на то пошло, использование динамически выделяемой
памяти). Память, используемая для старого значения \lstinline{*db*}, будет автоматически возвращена
системе, как только выяснится, что на неё больше ничто не ссылается.}.
\section{Избавление от дублирующего кода и большой выигрыш}
До сих пор весь код базы данных, обеспечивающий операции \lstinline{insert}, \lstinline{select},
\lstinline{update} и \lstinline{delete}, если не считать интерфейс командной строки для добавления
новых записей и распечатки содержимого базы, укладывался в немногим более пятидесяти
строк. Целиком\pclfootnote{Мой друг однажды проводил собеседование с кандидатом на должность
программиста и задал ему обычный вопрос, распространённый на таких собеседованиях: <<Как
вы понимаете, что функция (или метод) стала слишком велика?>> <<Ну...~-- ответил
кандидат.~-- Я стараюсь делать так, чтобы любой метод был меньше, чем моя голова>>.~-- <<Вы
хотите сказать, что не можете удержать в голове всех деталей?>>~-- <<Нет, я хочу сказать,
что сажусь перед монитором, и код не должен быть больше, чем моя голова>>.}.
Всё ещё существует некоторое раздражающее дублирование кода. И, оказывается, вы можете
избавиться от этого дублирования, в то же время сделав код более гибким. Дублирование, о
котором я говорю, находится в функции \lstinline{where}. Тело функции \lstinline{where}~-- набор
условий для каждого поля, таких, как это:
\begin{myverb}
(if title (equal (getf cd :title) title) t)
\end{myverb}
Сейчас это не так плохо, но, как и во многих случаях дублирования кода, за это всегда
приходится платить одну цену: если вы хотите изменить работу этого кода, вам нужно
изменять множество копий. И если вы изменили поля в CD, вам придётся добавить или удалить
условия для \lstinline{where}. \lstinline{update} страдает точно таким же дублированием. Это,
несомненно, плохо, так как весь смысл функции \lstinline{where} заключается в динамической
генерации куска кода, проверяющего нужные нам значения; почему она должна производить
работу во время выполнения, каждый раз проверяя, было ли ей передано значение
\lstinline{title}?
Представьте, что вы попытались оптимизировать этот код и обнаружили, что много времени
тратится на проверку того, заданы ли значения \lstinline{title} и оставшиеся
ключ-параметры\footnote{Вряд ли проверка того, был ли ключ-параметр передан в функцию,
приведёт к существенному падению производительности, так как проверка значения
переменной на \lstinline{NIL}~-- очень <<дешёвая>>, с точки зрения производительности, операция. С
другой стороны, эти функции, возвращаемые \lstinline{where}, оказываются как раз в середине
внутреннего цикла вызовов \lstinline{select}, \lstinline{update} и \lstinline{delete-rows}, так как
вызываются для каждой записи в базе данных. В~любом случае, для наглядности пусть будет
так.}\hspace{\footnotenegspace}. Если вы на самом деле хотите избавиться от этих проверок во время выполнения, товы
можете просмотреть программу и найти все места, где вы вызываете \lstinline{where}, и
посмотреть, какие аргументы вы передаёте. Затем вы можете заменить каждый вызов
\lstinline{where} анонимной функцией, выполняющей только необходимые вычисления. Например, если
вы нашли такой кусок кода:
\begin{myverb}
(select (where :title "Give Us a Break" :ripped t))
\end{myverb}
\noindent{}вы можете заменить его на такой:
\begin{myverb}
(select
#'(lambda (cd)
(and (equal (getf cd :title) "Give Us a Break")
(equal (getf cd :ripped) t))))
\end{myverb}
Заметьте, что анонимная функция отличается от той, что возвращает \lstinline{where}; мы не
пытаемся сохранить вызов \lstinline{where}, а обеспечиваем большую производительность функции
выбора. Эта анонимная функция имеет условия только для нужных нам полей, и она не
производит дополнительной работы, в отличие от функции, которую может возвратить
\lstinline{where}.
Вы можете представить себе, что значит пройтись по всему исходному тексту, исправить все
вызовы \lstinline{where} таким образом. И вы можете представить, насколько это болезненно. Если
бы этого было достаточно и это было бы очень важно, вероятно, стоило бы написать
некоторого рода препроцессор, который бы конвертировал вызовы \lstinline{where} в то, что вы бы
написали вручную.
Средство Lisp, позволяющее делать это очень просто, называется системой
макросов. Подчёркиваю, что макрос в Common Lisp не имеет, в сущности, ничего общего (кроме
имени) с текстовыми макросами из C и C++. В~то время как препроцессор C оперирует
текстовой подстановкой и не знает ничего о структуре C и C++, в Lisp макрос, в сущности,
является генератором кода, который автоматически запускается для вас
компилятором\pclfootnote{Макросы также выполняются интерпретатором, тем не менее
сущность макросов легче понять, когда думаешь о компилируемом коде. Как и обо всём
остальном в этой главе, я расскажу об этом более подробно в следующих главах.}. Когда
выражение на Lisp содержит вызов макроса, компилятор Lisp вместо вычисления аргументов и
передачи их в функцию передаёт аргументы, не вычисляя их, в код макроса, который, в свою
очередь, возвращает новое выражение на Lisp, которое затем вычисляется в месте исходного
вызова макроса.
Я начну с простого и глупого примера и затем покажу, как вы можете заменить функцию
\lstinline{where} макросом \lstinline{where}. Перед тем как я напишу этот макрос-пример, мне
необходимо представить вам одну новую функцию: \lstinline{REVERSE} принимает аргумент в виде
списка и возвращает новый список, который является обратным к исходному. Таким образом,
\lstinline{(reverse '(1 2 3))} вернёт \lstinline{(3 2 1)}. Теперь попробуем создать макрос.
\begin{myverb}
(defmacro backwards (expr)
(reverse expr))
\end{myverb}
Главное синтаксическое отличие между функцией и макросом заключается в том, что макрос
определяется ключевым словом \lstinline{DEFMACRO}, а не \lstinline{DEFUN}. После ключевого слова в
определении макроса, подобно определению функции, следуют имя, список параметров и тело с
выражениями. Однако макросы действуют совершенно по-другому. Вы можете использовать макрос
так:
\begin{myverb}
CL-USER> (backwards ("hello, world" t format))
hello, world
NIL
\end{myverb}
Как это работает? Когда REPL начинает вычислять выражение \lstinline{backwards}, он
обнаруживает, что \lstinline{backwards}~-- имя макроса. Поэтому он не вычисляет выражение
\lstinline{("hello, world" t format)}, что очень хорошо, так как это некорректная для Lisp
структура. Далее он передаёт этот список коду \lstinline{backwards}. Код \lstinline{backwards}
передаёт список в функцию \lstinline{REVERSE}, которая возвращает список
\lstinline{(format t "hello, world")}. Затем \lstinline{backwards} передаёт это значение обратно REPL, который
подставляет его на место исходного выражения.
Макрос \lstinline{backwards}, таким образом, определяет новый язык, во многом похожий на
Lisp,~-- только задом наперёд~-- который вы можете вставлять в свой код в любой момент,
просто обернув обратное выражение на Lisp в вызов макроса \lstinline{backwards}. И в
скомпилированной программе на Lisp этот новый язык покажет такую же производительность,
как и обычный Lisp, потому что весь код в макросе~-- код, сгенерированный в новом
выражении,~-- выполняется во время компиляции. Другими словами, компилятор сгенерирует один
и тот же код, независимо от того, напишете вы \lstinline{(backwards ("hello, world" t format))}
или \lstinline{(format t "hello, world")}.
Итак, как это поможет решить проблему дублирующегося кода в \lstinline{where}? Очень прос\-то. Вы
можете написать макрос, генерирующий совершенно такой же код, какой вы написали бы для
каждого вызова \lstinline{where}. И снова лучший подход~-- это разрабатывать код снизу
вверх. В~оптимизированной вручную функции выбора \lstinline{where} для каждого из заданных
полей у вас было выражение в следующей форме:
\begin{myverb}
(equal (getf cd field) value)
\end{myverb}
Давайте напишем функцию, которая, получив имя поля и некоторое значение, возвращает такое
выражение. Так как выражение~-- это просто список, вы можете подумать, что возможно
написать что-нибудь вроде:
\begin{myverb}
(defun make-comparison-expr (field value) ; неправильно
(list equal (list getf cd field) value))
\end{myverb}
Однако здесь имеется небольшой нюанс: как вы знаете, когда Lisp обнаруживает просто имя
вроде \lstinline{field} или \lstinline{value}, а не первый элемент списка, он полагает, что это имя
переменной, и пытается получить её значение. Это нормально для \lstinline{field} и
\lstinline{value}; это именно то, что нужно. Но он будет обращаться к \lstinline{equal}, \lstinline{getf}
и \lstinline{cd} таким же образом, а это в нашем случае нежелательно. Вы, однако, знаете также,
как не позволить Lisp пытаться вычислить структуру: поместить перед ней одиночную кавычку
(\lstinline{'}). Таким образом, если вы напишете функцию \lstinline{make-comparison-expr} вот так,
она сделает то, что вам нужно:
\begin{myverb}
(defun make-comparison-expr (field value)
(list 'equal (list 'getf 'cd field) value))
\end{myverb}
Вы можете проверить её работу в REPL:
\begin{myverb}
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")
\end{myverb}
Но, оказывается, существует лучший способ сделать это. То, что вам действительно нужно,~--
это иметь возможность написать выражение, которое в большинстве случаев не вычисляется, и
затем каким-либо образом выбирать некоторые выражения, которые вы хотите вычислить. И
конечно же, такой механизм существует. Обратная кавычка (\lstinline{`}) перед выражением
запрещает его вычисление, точно так же, как и прямая одиночная кавычка.
\begin{myverb}
CL-USER> `(1 2 3)
(1 2 3)
CL-USER> '(1 2 3)
(1 2 3)
\end{myverb}
Однако в выражении с обратной кавычкой любое подвыражение, перед которым стоит запятая,
вычисляется. Обратите внимание на влияние запятой во втором выражении:
\begin{myverb}
`(1 2 (+ 1 2)) ==> (1 2 (+ 1 2))
`(1 2 ,(+ 1 2)) ==> (1 2 3)
\end{myverb}
Используя обратную кавычку, вы можете переписать функцию \lstinline{make-comparison-expr} следующим образом:
\begin{myverb}
(defun make-comparison-expr (field value)
`(equal (getf cd ,field) ,value))
\end{myverb}
Теперь, если вы посмотрите на оптимизированную вручную функцию выбора, то увидите, что
тело функции состоит из одного оператора сравнения для каждой пары поле/значение,
обернутого в выражение \lstinline{AND}. На мгновение предположим, что вам нужно расположить
аргументы таким образом, чтобы передать их макросу \lstinline{where} единым списком. Вам
понадобится функция, которая принимает аргументы этого списка попарно и сохраняет
результаты выполнения вызова \lstinline{make-comparison-expr} для каждой пары. Чтобы
реализовать эту функцию, вы можете воспользоваться мощным макросом \lstinline{LOOP}.
\begin{myverb}
(defun make-comparisons-list (fields)
(loop while fields
collecting (make-comparison-expr (pop fields) (pop fields))))
\end{myverb}
Полное описание макроса \lstinline{LOOP} отложим до главы~\ref{ch:22}, а сейчас заметим, что выражение
\lstinline{LOOP} выполняет именно то, что требуется: оно циклично проходит по всем элементам в
списке \lstinline{fields}, каждый раз возвращая по два элемента, передаёт их в
\lstinline{make-comparison-expr} и сохраняет возвращаемые результаты, чтобы их вернуть при
выходе из цикла. Макрос \lstinline{POP} выполняет операцию, обратную операции, выполняемой макросом
\lstinline{PUSH}, который вы использовали для добавления записей в \lstinline{*db*}.
Теперь вам нужно просто обернуть список, возвращаемый функцией \lstinline{make-comparison-list},
в \lstinline{AND} и анонимную функцию, которую вы можете реализовать прямо в макросе
\lstinline{where}. Это несложно: используйте обратную кавычку, чтобы создать шаблон, который
будет заполнен значениями функции \lstinline{make-comparison-list}.
\begin{myverb}
(defmacro where (&rest clauses)
`#'(lambda (cd) (and ,@(make-comparisons-list clauses))))
\end{myverb}
Этот макрос использует вариацию \lstinline{,} (а именно \lstinline{,@}) перед вызовом
\lstinline{make-comparison-list}. Сочетание \lstinline{,@} <<вклеивает>> значение следующего за ним
выражения, которое должно возвращать список, во <<внешний>> список.
\begin{myverb}
`(and ,(list 1 2 3)) ==> (AND (1 2 3))
`(and ,@(list 1 2 3)) ==> (AND 1 2 3)
\end{myverb}
Вы также можете использовать \lstinline{,@} для <<вклейки>> элементов в середину списка:
\begin{myverb}
`(and ,@(list 1 2 3) 4) ==> (AND 1 2 3 4)
\end{myverb}
Другая важная особенность макроса \lstinline{where}~-- использование \lstinline{&rest} в списке
аргументов. Так же, как и \lstinline!&key!, \lstinline{&rest} изменяет способ разбора
аргументов. Если в списке параметров обнаруживается \lstinline{&rest}, функция или макрос может
принимать произвольное число аргументов, которые собираются в единый список, становящийся
значением переменной, имя которой следует за \lstinline{&rest}. Итак, если вы вызовите
\lstinline{where} так:
\begin{myverb}
(where :title "Give Us a Break" :ripped t)
\end{myverb}
\noindent{}переменная \lstinline{clauses} будет содержать список:
\begin{myverb}
(:title "Give Us a Break" :ripped t)
\end{myverb}
Этот список передаётся функции \lstinline{make-comparisons-list}, которая возвращает список
выражений сравнения. С помощью функции \lstinline{MACROEXPAND-1} вы можете точно видеть,
какой код будет сгенерирован \lstinline{where}. Если вы передадите в \lstinline{MACROEXPAND-1}
форму, являющуюся вызовом макроса, она вызовет макрос с заданными аргументами и вернёт его
развёрнутый вид. Итак, вы можете проверить предыдущий вызов \lstinline{where} следующим
образом:
\begin{myverb}
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
\end{myverb}
Выглядит неплохо. Теперь попробуем испытать макрос в действии:
\begin{myverb}
CL-USER> (select (where :title "Give Us a Break" :ripped t))
((:TITLE "Give Us a Break" :ARTIST "Limpopo" :RATING 10 :RIPPED T))
\end{myverb}
Работает. И макрос \lstinline{where} с его двумя функциями-помощниками оказался на одну строку
короче, чем старая функция \lstinline{where}. И что самое главное, \lstinline{where} больше не
привязана к конкретным полям наших записей о CD.
\section{Об упаковке}
Случилась интересная вещь. Вы избавились от дублирования и сделали код одновременно более
производительным и универсальным. Так часто бывает, если правильно выбрать макрос. Это
имеет смысл, потому что макрос~-- это ещё один механизм создания абстракций~--
абстракций на синтаксическом уровне, а абстракции~-- это, по определению, более короткий
путь для выражения подразумеваемых сущностей. Сейчас код мини-базы данных, который
относится к CD и полям, его описывающим, находится только в функциях \lstinline{make-cd},
\lstinline{prompt-for-cd} и \lstinline{add-cd}. Фактически наш новый макрос будет работать с любой
базой данных, основанной на списке свойств.
Тем не менее эта база данных всё ещё далека от завершения. Вероятно, вы думаете о
добавлении множества возможностей, например таких, как поддержка множества таблиц или
более сложных запросов. В~главе~\ref{ch:27} мы создадим базу данных о записях MP3, которая будет
содержать некоторые из этих возможностей.
Целью этой главы являлось быстрое введение в лишь малую часть возможностей Lisp и
демонстрация того, как они используются для написания кода, чуть более интересного, чем
\lstinline{"Hello, world"}. В~следующей главе мы начнём более систематический обзор Lisp.
%%% Local Variables:
%%% mode: latex
%%% TeX-master: "pcl-ru"
%%% TeX-open-quote: "<<"
%%% TeX-close-quote: ">>"
%%% End:
Jump to Line
Something went wrong with that request. Please try again.