Как начинался компьютер
Компьютерная революция
Двоичный код
Разработки военных лет
Интегральные микросхемы
Микрокомпьютер
Персоны
Сеть
Язык компьютера
Развитие ПО
Гибкие системы
Средства разработки
Информатика
Вычислительная наука
Операционные системы
Искусственный интеллект
Предыстория
Поиск
Знания и рассуждения
Логика
Робототехника
 

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

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

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

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

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

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

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

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