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

 
Язык Лисп Печать
  
Второй аргумент FORMAT — формат строки; он может как содержать символьный текст, так и управляющие команды, контроллирующие работу этой функции, например то, как она должна интерпретировать остальные аргументы. Команды, управляющие форматом вывода, начинаются со знака тильды (~) (так же, как управляющие команды начинаются с %). FORMAT может принимать довольно много таких команд, каждую со своим набором параметров. Однако сейчас я сфокусируюсь только на тех управляющих командах, которые необходимы для написания функции dump-db.

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

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

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

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

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

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