Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Branch: master
Fetching contributors…

Cannot retrieve contributors at this time

818 lines (665 sloc) 66.751 kB
\chapter{Несколько рецептов для функции FORMAT}
\label{ch:18}
\thispagestyle{empty}
Функция \lstinline{FORMAT} вместе с расширенным макросом \lstinline{LOOP}~-- одна из двух
возможностей Common Lisp, которые вызывают сильную эмоциональную реакцию у многих
пользователей Common Lisp. Некоторые их любят, другие ненавидят\footnote{Конечно,
большинство людей осознают, что не стоит возбуждаться по поводу чего-либо в языке
программирования, и используют или не используют их без сильного беспокойства. С другой
стороны, интересно, что две эти возможности~-- единственные в Common Lisp, которые
реализуют, по сути, языки предметной области (domain specific languages), используя
синтаксис, не основанный на s-выражениях. Синтаксис управляющих строк \lstinline{FORMAT}
основан на символах, в то время как расширенный макрос \lstinline{LOOP} может быть понят
только в терминах грамматики ключевых слов \lstinline{LOOP}. Одна из обычных придирок к
\lstinline{FORMAT} и \lstinline{LOOP}~-- то, что они <<недостаточно лисповские>>.~--
является доказательством того, что Lisp-программистам действительно нравится синтаксис,
основанный на s-выражениях.}\hspace{\footnotenegspace}.
Поклонники \lstinline{FORMAT} любят её за мощь и краткость, в то время как противники её
ненавидят за потенциал для возникновения ошибок и непрозрачность. Сложные управляющие
строки \lstinline{FORMAT} имеют иногда подозрительное сходство с помехами на
экране\translationnote{В~оригинале~-- line noise, шум в линии, имеется в виду схожесть
строки формата с сигналом на осциллографе.}, но \lstinline{FORMAT} остаётся популярным
среди программистов на Common Lisp, которые хотят формировать небольшие куски
удобочитаемого текста без необходимости нагромождать кучи формирующего вывод кода. Хотя
управляющие строки \lstinline{FORMAT} могут быть весьма замысловаты, но во всяком случае
единственное \lstinline{FORMAT}-выражение врядли сильно замусорит ваш код. Предположим,
например, что вы хотите напечатать значения в списке, разделённые запятыми. Вы можете
написать так:
\begin{myverb}
(loop for cons on list
do (format t "~a" (car cons))
when (cdr cons) do (format t ", "))
\end{myverb}
Это не очень плохо, но любой, кто будет читать этот код, должен будет мысленно
проанализировать его, прежде чем понять, что все, что он делает,~-- это печать содержимого
списка \lstinline{list} на стандартный вывод. С другой стороны, вы можете с одного взгляда
определить, что следующее выражение печатает список в некоторой форме на стандартный
вывод:
\begin{myverb}
(format t "~{~a~^, ~}" list)
\end{myverb}
Если вам важна форма, которую примет вывод, тогда вы можете изучить управ\-ляю\-щую строку, но
если все, что вы хотите,~-- это приблизительно знать, что делает эта строка кода, то это
можно увидеть сразу.
Во всяком случае, вам следует по меньшей мере зрительно воспринимать \lstinline{FORMAT}, а ещё
лучше разобраться, на что она способна, прежде чем вы примкнёте к про- или
анти-\lstinline{FORMAT}'овскому лагерю. Также важно понимать по меньшей мере основы
\lstinline{FORMAT}, поскольку другие стандартные функции, такие как функции выбрасывания
условий, обсуждаемые в следующей главе, используют управляющие строки в стиле
\lstinline{FORMAT} для формирования вывода.
Чтобы сильнее запутать дело, \lstinline{FORMAT} поддерживает три совершенно разных вида
форматирования: печать таблиц с данными, структурная печать (\textit{pretty printing})
s-выражений и формирование удобочитаемых сообщений со вставленными (interpolated)
значениями. Печать таблиц с текстовыми данными на сегодня несколько устарела; это одно из
напоминаний, что Lisp стар, как FORTRAN. В~действительности некоторые директивы, которые
вы можете использовать для печати значений с плавающей точкой внутри полей с фиксированной
длиной, были основаны прямо на дескрипторах редактирования (edit descriptors) FORTRAN,
которые использовались в FORTRAN для чтения и печати столбцов с данными, расположенными
внутри полей с фиксированной длинной. Тем не менее использование Common Lisp как замены
FORTRAN выходят за рамки этой книги, так что я не буду обсуждать эти аспекты
\lstinline{FORMAT}.
Структурная печать также находится за рамками этой книги~-- не потому, что она устарела, а
потому, что это слишком большая тема. Вкратце механизм структурной печати Common Lisp~--
это настраиваемая система для печати блочно-структурированных данных, включая, но не
ограничиваясь, s-выражения, применяющаяся, когда необходимы переменные отступы и
динамически увеличивающиеся переводы строк. Это отличный инструмент при необходимости, но
не часто требуемый в повседневном программировании\pclfootnote{Читатели, интересующиеся
внутренним устройством структурной печати, могут почитать статью <<XP: A Common Lisp
Pretty Printing System>> Ричарда Уотерса (Richard Waters). Это описание системы
структурированной печати, которая в итоге была включена в Common Lisp. Вы можете
загрузить её с \url{ftp://publications.ai.mit.edu/ai-publications/pdf/AIM-1102a.pdf}.}.
Вместо этого я сосредоточусь на частях \lstinline{FORMAT}, которые вы можете использовать,
чтобы формировать удобочитаемые строки со вставленными в них значениями. Даже ограничивая
обзор таким образом, остаётся изрядное количество материала. Не чувствуйте себя так, как
будто вы обязаны помнить каждую деталь, описанную в этой главе. Вы можете достичь многого
с лишь несколькими идиомами \lstinline{FORMAT}. Сначала я опишу наиболее важные возможности
\lstinline{FORMAT}; а вам решать, насколько вы хотите стать волшебником \lstinline{FORMAT}.
\section{Функция FORMAT}
Как вы видели в предыдущих главах, функция \lstinline{FORMAT} принимает два аргумента:
получатель для своего вывода и управляющую строку, которая содержит буквенный текст и
вложенные \textit{директивы}. Любые дополнительные аргументы предоставляют значения,
используемые директивами управляющей строки, которые вставляют эти значения в печатаемый
текст. Я буду ссылаться на эти аргументы как на \textit{аргументы формата}.
Первым аргументом \lstinline{FORMAT}, получателем для печатаемого текста, может быть
\lstinline{T}, \lstinline{NIL}, поток или строка с указателем заполнения. \lstinline{Т}
обозначает поток \lstinline{*STANDARD-OUTPUT*}, в то время как \lstinline{NIL} заставляет
\lstinline{FORMAT} сформировать свой вывод в виде строки, которую функция затем
возвращает\footnote{Чтобы немного запутать дело, большинство остальных функций
ввода/вывода также принимают~\lstinline{T} и \lstinline{NIL} как указатели потоков, но с
небольшим отличием: указатель потока~\lstinline{Т} обозначает двунаправленный поток
\lstinline{*TERMINAL-IO*}, тогда как \lstinline{NIL} указывает на
\lstinline{*STANDARD-OUTPUT*} как на стандартный поток вывода и
\lstinline{*STANDARD-INPUT*} как на стандартный поток ввода.}\hspace{\footnotenegspace}. Если получатель~--
поток, то вывод пишется в поток. А если получатель~-- строка с указателем заполнения, то
форматированный вывод добавляется к концу строки и указатель заполнения соответственно
выравнивается. За исключением случая, когда получатель~-- \lstinline{NIL} и функция
возвращает строку, \lstinline{FORMAT} возвращает \lstinline{NIL}.
Второй аргумент~-- управляющая строка, является, в сущности, программой на языке
\lstinline{FORMAT}. Язык \lstinline{FORMAT} не целиком <<лисповый>>~-- его основной синтаксис основан
на символах, а не на s-выражениях, и оптимизирован для краткости, а не для лёгкого
понимания. Вот почему сложные управляющие строки \lstinline{FORMAT} могут быть приняты за
помехи.
Большинство из директив \lstinline{FORMAT} просто вставляют аргумент внутрь выводимого текста в
той или иной форме. Некоторые директивы, такие как \lstinline!~%!, которая заставляет
\lstinline{FORMAT} выполнить перевод строки, не используют никаких аргументов. Другие, как вы
увидите, могут использовать более одного аргумента. Одна из директив даже позволяет вам
прыгать по списку аргументов с целью обработки одного и того же аргумента несколько раз,
или в некоторых ситуациях пропустить определённые аргументы. Но прежде чем я буду
обсуждать конкретные директивы, давайте взглянем на общий синтаксис директив.
\section{Директивы FORMAT}
Все директивы начинаются с тильды (\lstinline!~!) и заканчиваются отдельным знаком, который
идентифицирует директиву. Вы можете писать этот символ как в верхнем, так и в нижнем
регистре. Некоторые директивы принимают \textit{префиксные параметры}, которые пишутся
непосредственно после тильды, разделяются запятыми и используются для управления такими
деталями, как сколько разрядов печатать после десятичной точки при печати числа с
плавающей точкой. Например, директива \lstinline!~$!, одна из директив, использующихся
для печати значений c плавающей точкой, по умолчанию печатает два разряда, следующие за
десятичной точкой.%$
\begin{myverb}
CL-USER> (format t "~$" pi)
3.14
NIL
\end{myverb}
Тем не менее с префиксным параметром вы можете указать, чтобы функция печатала свой
аргумент, к примеру с пятью десятичными знаками, как здесь:
\begin{myverb}
CL-USER> (format t "~5$" pi)
3.14159
NIL
\end{myverb}
Значениями префиксного параметра являются либо числа, записанные как десятичные, либо
знаки, записанные в виде одинарной кавычки, за которой следует нужный символ. Значение
префиксного параметра может быть также получено из аргумента формата двумя способами:
префиксный параметр \lstinline{v} заставляет \lstinline{FORMAT} использовать один аргумент формата и
назначить его значение префиксному параметру. Префиксный параметр \lstinline!#! будет вычислен
как количество оставшихся аргументов формата. Например:
\begin{myverb}
CL-USER> (format t "~v$" 3 pi)
3.142
NIL
CL-USER> (format t "~#$" pi)
3.1
NIL
\end{myverb}
Я дам более правдоподобные примеры использования аргумента \lstinline!#! в
разделе <<Условное форматирование>>.
Вы можете также опустить оба префиксных параметра. Впрочем, если вы хотите указать один
параметр, но не желаете указывать параметры, стоящие перед ним, то вы должны включить
запятую для каждого пропущенного параметра. Например, директива \lstinline!~F!, другая
директива для печати значений с плавающей точкой, также принимает параметр для управления
количеством десятичных разрядов при печати, но это второй по счёту параметр, а не
первый. Если вы хотите использовать \lstinline!~F! для печати числа с пятью десятичными
разрядами, вы можете написать так:
\begin{myverb}
CL-USER> (format t "~,5f" pi)
3.14159
NIL
\end{myverb}
Также вы можете изменить поведение некоторых директив при помощи \textit{модификаторов}
\lstinline!:! и знака \lstinline!@!, которые ставятся после любого префиксного параметра и
до идентифицирующего директиву знака. Эти модификаторы незначительно меняют поведение
директивы. Например, с модификатором \lstinline!:! директива \lstinline!~D!,
использующаяся для вывода целых чисел в десятичном виде, создаёт число с запятыми,
разделяющими каждые три разряда, в то время как знак \lstinline!@! заставляет
\lstinline!~D! включить знак плюс в случае положительного числа.
\begin{myverb}
CL-USER> (format t "~d" 1000000)
1000000
NIL
CL-USER> (format t "~:d" 1000000)
1,000,000
NIL
CL-USER> (format t "~@d" 1000000)
+1000000
NIL
\end{myverb}
В~случае необходимости вы можете объединить модификаторы \lstinline!:! и \lstinline!@!,
для того чтобы получить оба варианта:
\begin{myverb}
CL-USER> (format t "~:@d" 1000000)
+1,000,000
NIL
\end{myverb}
В~директивах, где оба модифицированных варианта поведения не могут быть осмысленно
объединены, использование обоих модификаторов либо не определено, либо приобретает третье
значение.
\vfill{}
\section{Основы форматирования}
Сейчас вы готовы познакомиться с конкретными директивами. Я начну с нескольких наиболее
распространённых директив, включая несколько тех, которые вы уже встречали в предыдущих
главах.
Наиболее универсальная директива~-- \lstinline!~A!, которая использует один аргумент
формата любого типа и печатает его в \textit{эстетичной} (удобочитаемой) форме. Например,
строки печатаются без кавычек и экранирующих символов (escape characters), а числа
печатаются в форме, принятой для соответствующего числового типа. Если вы хотите просто
получить значение, предназначенное для прочтения человеком, то эта директива~-- ваш выбор.
\begin{myverb}
(format nil "The value is: ~a" 10) ==> "The value is: 10"
(format nil "The value is: ~a" "foo") ==> "The value is: foo"
(format nil "The value is: ~a" (list 1 2 3)) ==> "The value is: (1 2 3)"
\end{myverb}
Родственная директива \lstinline!~S! также требует один аргумент формата любого типа и
печатает его. Однако \lstinline!~S! пытается сформировать такой вывод, который мог бы
быть прочитан обратно с помощью \lstinline{READ}. Поэтому строки должны быть заключены в
кавычки, при необходимости знаки должны быть специлизированы для пакета
(package-qualified), и~т.д. Объекты, которые не имеют подходящего для \lstinline{READ}
представления, печатаются в виде непригодном для чтения: \lstinline!#<>!. С модификатором
\textit{двоеточие} обе директивы \lstinline!~A! и \lstinline!~S! выводят
\lstinline{NIL} в виде \lstinline{()}, а не как \lstinline{NIL}. Обе директивы
\lstinline!~A! и \lstinline!~S! также принимают до четырёх префиксных параметров, которые
могут быть использованы для выравнивания пробелами (padding), добавляемыми после
(или до, при модификаторе \textit{@}) значения, впрочем эти функции действительно полезны
лишь при формировании табличных данных.
Две другие часто используемые директивы~-- это \lstinline!~%!, которая создаёт новую
строку, и \lstinline!~&!, которая выполняет \textit{перевод строки}. Разница между ними в
том, что \lstinline!~%! всегда создаёт новую строку, тогда как \lstinline!~&!.
срабатывает только если она уже не находится в начале строки. Это удобно при создании
слабо связанных функций, каждая из которых формирует кусок текста и их нужно объединить
различными способами. Например, если одна из функции выводит текст, который заканчивается
новой строкой (\lstinline!~%!), а другая функция выводит некоторый текст, который
начинается с перевода строки (\lstinline!~&!), то вам не стоит беспокоиться о получении
дополнительной пустой строки, если вы вызываете их одну за другой. Обе эти директивы могут
принимать единственный префиксный параметр, который обозначает количество выводимых новых
строк. Директива \lstinline!~%! просто выведет заданное количество знаков новой строки, в
то время как директива \lstinline!~&! создаст либо \lstinline{n - 1}, либо \lstinline{n}
новых строк, в зависимости от того, начинает ли она с начала строки.
Реже используется родственная им директива \lstinline!~~!, которая заставляет
\lstinline{FORMAT} вывести знак тильды. Подобно директивам \lstinline!~%! и \lstinline!~&!,
она может быть параметризована числом, которое задаёт количество выводимых тильд.
\section{Директивы для знаков и целых чисел}
\label{ch18:chars-numbers}
Вдобавок к директивам общего назначения \lstinline!~A! и \lstinline!~S! \lstinline{FORMAT}
поддерживает несколько директив, которые могут использоваться для получения значений
определённых типов особыми способами. Простейшей из них является директива \lstinline!~C!,
которая используется для вывода знаков. Ей не требуются префиксные аргументы, но её работу
можно корректировать с помощью модификаторов \textit{двоеточие} и \textit{@}. Без
модификаций её поведение на отличается от поведения \lstinline!~A!, за исключением того,
что она работает только со знаками. Модифицированные версии более полезны. С модификатором
\textit{двоеточие} \lstinline!~:C! выводит заданные по имени \textit{непечатаемые} знаки,
такие как пробел, символ табуляции и перевод строки. Это полезно, если вы хотите создать
сообщение к пользователю, в котором упоминается некоторый символ. Например, следующий код:
\begin{myverb}
(format t "Syntax error. Unexpected character: ~:c" char)
\end{myverb}
\noindent{}может напечатать такое сообщение:
\begin{myverb}
Syntax error. Unexpected character: a
\end{myverb}
\noindent{}а ещё вот такое:
\begin{myverb}
Syntax error. Unexpected character: Space
\end{myverb}
Вместе с модификатором \lstinline!@! \lstinline!~@С! выведет знак в синтаксисе знаков
Lisp:
\begin{myverb}
CL-USER> (format t "~@c~%" #\bslash{}a)
#\bslash{}a
NIL
\end{myverb}
Одновременно с модификаторами \lstinline!:! и \textit{@} директива \lstinline!~C! может
напечатать дополнительную информацию о том, как ввести символ с клавиатуры, если для этого
требуется специальная клавиатурная комбинация. Например, на Macintosh в некоторых
приложениях вы можете ввести нулевой символ (код символа 0 в ASCII или в любом
надмножестве ASCII, наподобие ISO-8859-1 или Unicode), удерживая клавиши Ctrl и нажав
'\lstinline!@!'. В~OpenMCL, если вы напечатаете нулевой символ c помощью директивы
\lstinline!~:C!, она сообщит вам следующее:
\begin{myverb}
(format nil "~:@c" (code-char 0)) ==> "^@ (Control @)"
\end{myverb}
Однако не все версии Lisp реализуют этот аспект директивы \lstinline!~C!. Даже если они
это делают, то реализация может и не быть аккуратной~-- например, если вы за\-пусти\-те OpenMCL
в SLIME, комбинация клавиш \lstinline!C-@! перехватывается Emacs, вызывая команду
\lstinline{set-mark-command}\footnote{Этот вариант директивы \lstinline!~C! имеет большее
значение на платформах наподобие Lisp-машин, где нажатие клавиши представляется знаками
Lisp.}\hspace{\footnotenegspace}.
Другой важной категорией являются директивы формата, предназначенные для вывода
чисел. Хотя для этого вы можете использовать директивы \lstinline!~A! и \lstinline!~S!, но
если вы хотите получить над печатью чисел больший контроль, вам следует использовать одну
из специально предназначенных для чисел директив. Ориентированные на числа директивы могут
быть подразделены на две категории: директивы для форматирования целых чисел и директивы
для форматирования чисел с плавающей точкой.
Вот пять родственных директив для форматированного вывода целых чисел: \lstinline!~D!,
\lstinline!~X!, \lstinline!~O!, \lstinline!~B! и \lstinline!~R!. Чаще всего применяется
директива \lstinline!~D!, которая выводит целые числа по основанию \lstinline{10}.
\begin{myverb}
(format nil "~d" 1000000) ==> "1000000"
\end{myverb}
Как я упоминал ранее, с модификатором \lstinline!:! она добавляет запятые.
\begin{myverb}
(format nil "~:d" 1000000) ==> "1,000,000"
\end{myverb}
А с модификатором \lstinline!@! она всегда будет печатать знак.
\begin{myverb}
(format nil "~@d" 1000000) ==> "+1000000"
\end{myverb}
И оба модификатора могут быть объединены.
\begin{myverb}
(format nil "~:@d" 1000000) ==> "+1,000,000"
\end{myverb}
С помощью первого префиксного параметра можно задать минимальный размер вывода, а с
помощью второго~-- используемый символ заполнения. По умолчанию символ заполнения~-- это
пробел, и заполнение всегда помещается до самого числа.
\begin{myverb}
(format nil "~12d" 1000000) ==> " 1000000"
(format nil "~12,'0d" 1000000) ==> "000001000000"
\end{myverb}
Эти параметры удобны для форматирования объектов наподобие календарных дат в формате с
фиксированной длиной.
\begin{myverb}
(format nil "~4,'0d-~2,'0d-~2,'0d" 2005 6 10) ==> "2005-06-10"
\end{myverb}
Третий и четвёртый параметры используются в связке с модификатором \textit{двоеточие}:
третий параметр определяет знак, используемый в качестве разделителя между группами и
разрядами, а четвёртый параметр определяет число разрядов в группе. Их значения по
умолчанию~-- запятая и число 3 соответственно. Таким образом, вы можете использовать
директиву \lstinline!~:D! без параметров для вывода больших чисел в стандартном для
Соединённых Штатов формате, но можете заменить запятую на точку и группировку с \lstinline{3}
на \lstinline{4} с помощью \lstinline!~,,'.,4D!.
\begin{myverb}
(format nil "~:d" 100000000) ==> "100,000,000"
(format nil "~,,'.,4:d" 100000000) ==> "1.0000.0000"
\end{myverb}
Заметим, что вы должны использовать запятые для замещения позиций неуказанных параметров
длины и заполняющего символа, позволяя им сохранить свои значения по умолчанию.
Директивы \lstinline!~X!, \lstinline!~O!, и \lstinline!~B! работают подобно директиве
\lstinline!~D!, за исключением того, что они выводят числа в шестнадцатеричном,
восьмеричном и двоичном форматах.
\begin{myverb}
(format nil "~x" 1000000) ==> "f4240"
(format nil "~o" 1000000) ==> "3641100"
(format nil "~b" 1000000) ==> "11110100001001000000"
\end{myverb}
Наконец, директива \lstinline!~R!~-- универсальная директива для задания \textit{системы
счисления}. Её первый параметр~-- число между 2 и 36 (включительно), которое обозначает,
какое основание системы счисления использовать. Оставшиеся параметры такие же, что и
четыре параметра, принимаемые директивами \lstinline!~D!, \lstinline!~X!, \lstinline!~O! и
\lstinline!~B!, а модификаторы \textit{двоеточие} и \lstinline!@! меняют её поведение
схожим образом. Кроме того, директива \lstinline!~R! ведёт себя особым образом при
использовании без префиксных параметров, которые я буду обсуждать в разделе
<<\nameref{ch18:eng-lang}>>.
\section{Директивы для чисел с плавающей точкой}
Вот четыре директивы для форматирования значений с плавающей точкой: \lstinline!~F!,
\lstinline!~E!, \lstinline!~G! и~\lstinline!~$!. Первые три из них~-- это директивы,
основанные на дескрипторах редактирования (edit descriptor) FORTRAN. Я пропущу большинство
деталей этих директив, поскольку они в основном имеют дело с форматированием чисел с
плавающей точкой для использования в табличной форме. Тем не менее вы можете использовать
директивы \lstinline!~F!, \lstinline!~E! и~\lstinline!~$! для вставки значений с плавающей
точкой в текст. С другой стороны, директива \lstinline!~G!, или \textit{обобщенная}
директива, сочетает аспекты директив \lstinline!~F! и \lstinline!~E! единственным
осмысленным способом для печати таблиц.
Директива \lstinline!~F! печатает свой аргумент, который должен быть
числом\footnote{Технически, если аргумент не является вещественным числом, предполагается,
что \lstinline!~F! форматирует его так же, как это сделала бы директива ~D, которая
ведёт себя как директива \lstinline!~A!, если аргумент не является числом, но не все
реализации ведут себя должным образом.}\hspace{\footnotenegspace}, в десятичном формате, по возможности
контролируя количество разрядов после десятичной точки. Директиве \lstinline!~F! тем не
менее разрешается использовать компьютеризированное экспоненциальное представление, если
число достаточно велико либо мало. Директива \lstinline!~E!, с другой стороны, всегда
выводит числа в компьютеризированной научной нотации. Обе эти директивы принимают
несколько префиксных параметров, но вам нужно беспокоиться только о втором, который
управляет количеством разрядов, печатаемых после десятичной точки.
\begin{myverb}
(format nil "~f" pi) ==> "3.141592653589793d0"
(format nil "~,4f" pi) ==> "3.1416"
(format nil "~e" pi) ==> "3.141592653589793d+0"
(format nil "~,4e" pi) ==> "3.1416d+0"
\end{myverb}
Директива \lstinline!~$! (знак доллара) похожа на \lstinline!~F!, но несколько
проще. Как подсказывает её имя, она предназначена для вывода денежных единиц. Без
параметров она эквивалентна \lstinline!~,2F!. Чтобы изменить количество разрядов,
печатаемых после десятичной точки, используйте \textit{первый} параметр, в то время как
второй параметр регулирует минимальное количество разрядов, печатающихся до десятичной
дочки.%$
\begin{myverb}
(format nil "~$" pi) ==> "3.14"
(format nil "~2,4$" pi) ==> "0003.14"
\end{myverb}
С модификатором \lstinline!@! все три директивы, \lstinline!~F!, \lstinline!~E! и
\lstinline!~$!, можно заставить всегда печатать знак, плюс или минус\footnote{Итак, вот
что говорит стандарт языка. По какой-то причине, возможно коренящейся в общей
унаследованной кодовой базе, некоторые реализации Common Lisp реализуют этот аспект
директивы \lstinline!~F! некорректно.}\hspace{\footnotenegspace}. %$
\section{Директивы для английского языка}
\label{ch18:eng-lang}
Некоторые из удобнейших директив \lstinline{FORMAT} для формирования удобочитаемых
сообщений~-- те, которые выводят английский текст. Эти директивы позволяют вам выводить
числа как английский текст, выводить слова в множественном числе в зависимости от значения
аргумента формата и выполнять преобразование между строчными и прописными буквами в
секциях вывода \lstinline{FORMAT}.
Директива \lstinline!~R!, которую я обсуждал в разделе <<\nameref{ch18:chars-numbers}>>,
при использовании без указания системы счисления печатает числа как анг\-лийские слова или
римские цифры. При использовании без префиксного параметра и модификаторов она выводит
число словами как количественное числительное.
\begin{myverb}
(format nil "~r" 1234) ==> "one thousand two hundred thirty-four"
\end{myverb}
С модификатором \textit{двоеточие} она выводит число как порядковое числительное.
\begin{myverb}
(format nil "~:r" 1234) ==> "one thousand two hundred thirty-fourth"
\end{myverb}
И вместе с модификатором \lstinline!@! она выводит число в виде римских цифр; вместе с
\lstinline!@! и \textit{двоеточием} она выводит римские цифры, в которых четвёрки и
девятки записаны как \lstinline{IIII} и \lstinline{VIIII} вместо \lstinline{IV} и \lstinline{IX}.
\begin{myverb}
(format nil "~@r" 1234) ==> "MCCXXXIV"
(format nil "~:@r" 1234) ==> "MCCXXXIIII"
\end{myverb}
Для чисел, слишком больших, чтобы быть представленными в заданной форме, \lstinline!~R!
ведёт себя как \lstinline!~D!.
Чтобы помочь вам формировать сообщения со словами в нужном числе, \lstinline{FORMAT}
предоставляет директиву \lstinline!~P!, которая просто выводит 's', если соответствующий
аргумент не \lstinline{1}.
\begin{myverb}
(format nil "file~p" 1) ==> "file"
(format nil "file~p" 10) ==> "files"
(format nil "file~p" 0) ==> "files"
\end{myverb}
Тем не меннее обычно вы будете использовать \lstinline!~P! вместе с модификатором
\textit{двоеточие}, который заставляет её повторно обработать предыдущий аргумент формата.
\begin{myverb}
(format nil "~r file~:p" 1) ==> "one file"
(format nil "~r file~:p" 10) ==> "ten files"
(format nil "~r file~:p" 0) ==> "zero files"
\end{myverb}
С модификатором \lstinline!@!, который может быть объединён с модификатором
\textit{двоеточие}, \lstinline!~P! выводит \textit{y} или \textit{ies}.
\begin{myverb}
(format nil "~r famil~:@p" 1) ==> "one family"
(format nil "~r famil~:@p" 10) ==> "ten families"
(format nil "~r famil~:@p" 0) ==> "zero families"
\end{myverb}
Очевидно, что \lstinline!~P! не может разрешить все проблемы образования множественного
числа и не может помочь при формировании сообщений на других языках (отличных от
английского), она удобна в тех ситуациях, для которых предназначена. А директива
\lstinline!~[!, о которой я расскажу очень скоро, предоставит вам более гибкий способ
параметризации вывода \lstinline{FORMAT}.
Последняя директива, посвящённая выводу английского текста,~-- это \lstinline!~(!, которая
позволяет вам контролировать регистр выводимого текста. Каждая \lstinline!~(! составляет
пару с \lstinline!~)!, и весь вывод, сгенерированный частью управляющей строки между двумя
маркерами, будет преобразован в нижний регистр.
\begin{myverb}
(format nil "~(~a~)" "FOO") ==> "foo"
(format nil "~(~@r~)" 124) ==> "cxxiv
\end{myverb}
Вы можете изменить поведение \lstinline!~(! модификатором \lstinline!@!, чтобы заставить
ее начать с прописной буквы первое слово на участке текста, с \textit{двоеточием}
заставить её печатать все слова с прописной буквы, а с обоими модификаторами~--
преобразовать весь текст в верхний регистр. (Подходящие для этой директивы \textit{слова}
представляют собой буквенно-цифровые знаки, ограниченные небуквенно-цифровыми знаками или
концом текста.)
\begin{myverb}
(format nil "~(~a~)" "tHe Quick BROWN foX") ==> "the quick brown fox"
(format nil "~@(~a~)" "tHe Quick BROWN foX") ==> "The quick brown fox"
(format nil "~:(~a~)"p "tHe Quick BROWN foX") ==> "The Quick Brown Fox"
(format nil "~:@(~a~)" "tHe Quick BROWN foX") ==> "THE QUICK BROWN FOX"
\end{myverb}
\vfill{}
\section{Условное форматирование}
Вдобавок к директивам, вставляющим в выводимый текст свои аргументы и видоизменяющими
прочий вывод, \lstinline{FORMAT} предоставляет несколько директив, которые реализуют простые
управляющие структуры внутри управляющих строк. Одна из них, которую вы использовали в
главе~\ref{ch:09},-- это \textit{условная} директива \lstinline!~[!. Эта директива
замыкается соответствующей директивой \lstinline!~]!, а между ними находятся выражения,
разделённые \lstinline!~;!. Работа директивы \lstinline!~[!~-- выбрать одно из выражений,
которое затем обрабатывается в \lstinline{FORMAT}. Без модификаторов или параметров выражение
выбирается по числовому индексу; директива \lstinline!~[! использует аргумент формата,
который должен быть числом, и выбирает \textit{N}-ное (считая от нуля) выражение, где
\textit{N}~-- значение аргумента.
\begin{myverb}
(format nil "~[cero~;uno~;dos~]" 0) ==> "cero"
(format nil "~[cero~;uno~;dos~]" 1) ==> "uno"
(format nil "~[cero~;uno~;dos~]" 2) ==> "dos"
\end{myverb}
Если значение аргумента больше, чем число выражений, то ничего не печатается.
\begin{myverb}
(format nil "~[cero~;uno~;dos~]" 3) ==> ""
\end{myverb}
Однако если последний разделитель выражений~-- это \lstinline!~:;! вместо \lstinline!~;,!
тогда последнее выражение служит выражением по умолчанию.
\begin{myverb}
(format nil "~[cero~;uno~;dos~:;mucho~]" 3) ==> "mucho"
(format nil "~[cero~;uno~;dos~:;mucho~]" 100) ==> "mucho"
\end{myverb}
Также можно определить выбранное выражение, используя префиксный параметр. Хотя было бы
глупо применять константу, записанную прямо в управляющей строке, вспомним, что знак
'\lstinline!#!', используемый в качестве префиксного параметра, означает число оставшихся
для обработки аргументов. Поэтому вы можете определить строку формата следующим образом:
\begin{myverb}
(defparameter *list-etc*
"~#[NONE~;~a~;~a and ~a~:;~a, ~a~]~#[~; and ~a~:;, ~a, etc~].")
\end{myverb}
\noindent{}и использовать её так:
\begin{myverb}
(format nil *list-etc*) ==> "NONE."
(format nil *list-etc* 'a) ==> "A."
(format nil *list-etc* 'a 'b) ==> "A and B."
(format nil *list-etc* 'a 'b 'c) ==> "A, B and C."
(format nil *list-etc* 'a 'b 'c 'd) ==> "A, B, C, etc."
(format nil *list-etc* 'a 'b 'c 'd 'e) ==> "A, B, C, etc."
\end{myverb}
Заметим, что управляющая строка в действительности содержит две \lstinline!~[~]!
директивы, обе из которых применяют \lstinline!#! для выбора используемого
выражения. Первая директива использует от нуля до двух аргументов, тогда как вторая
использует ещё один, если он есть. \lstinline{FORMAT} молча проигнорирует любые аргументы
сверх использованных во время обработки управляющей строки.
С модификатором \textit{двоеточие} \lstinline!~[! может содержать только два выражения;
директива использует единственный аргумент и обрабатывает первое выражение, если аргумент
\lstinline{NIL}, и второе выражение в противном случае. Вы уже использовали этот вариант
\lstinline!~[! в главе~\ref{ch:09} для формирования сообщений типа сработало/не сработало
(pass/fail), таких как это:
\begin{myverb}
(format t "~:[FAIL~;pass~]" test-result)
\end{myverb}
Заметим, что оба выражения могут быть пустыми, но директива должна содержать~\lstinline!~;!.
Наконец, с модификатором \lstinline!@! директива \lstinline!~[! может иметь только одно
выражение. Директива использует первый аргумент и, если он отличен от \lstinline{NIL},
обрабатывает выражение, при этом список аргументов восстанавливается заново, чтобы первый
аргумент был доступен для использования заново.
\begin{myverb}
(format nil "~@[x = ~a ~]~@[y = ~a~]" 10 20) ==> "x = 10 y = 20"
(format nil "~@[x = ~a ~]~@[y = ~a~]" 10 nil) ==> "x = 10 "
(format nil "~@[x = ~a ~]~@[y = ~a~]" nil 20) ==> "y = 20"
(format nil "~@[x = ~a ~]~@[y = ~a~]" nil nil) ==> ""
\end{myverb}
\section{Итерация}
Другая директива \lstinline{FORMAT}, мимоходом виденная вами,~-- это директива итерации
\lstinline!~{!. Эта директива сообщает \lstinline{FORMAT} перебрать элементы списка или
неявного списка аргументов формата.
Без модификаторов \lstinline!~{! принимает один аргумент формата, который должен являться списком. Подобно директиве
\lstinline!~[!, которой всегда соответствует директива \lstinline!~]!, директива \lstinline!~{! всегда имеет соответствующую замыкающую
\lstinline!~}!. Текст между двумя маркерами обрабатывается как управляющая строка, которая выбирает свой аргумент из
списка, полученного директивой \lstinline!~{!. \lstinline{FORMAT} будет циклически обрабатывать эту
управляющую строку до тех пор, пока в перебираемом списке не останется элементов. В~следующем примере \lstinline!~{!
принимает один аргумент формата, список \lstinline{(1 2 3)} и затем обрабатывает управляющую строку <<\lstinline!~a, !>>, повторяя,
пока все элементы списка не будут использованы.
\begin{myverb}
(format nil "~{~a, ~}" (list 1 2 3)) ==> "1, 2, 3, "
\end{myverb}
При этом раздражает, что при печати за последним элементом списка следуют запятая и пробел. Вы можете исправить
это директивой \lstinline!~^!; внутри тела \lstinline!~{! директива \lstinline!~^! заставляет итерацию немедленно остановиться и, если в
списке не остаётся больше элементов, прервать обработку оставшейся части управляющей строки. Таким образом, для
предотвращения печати запятой и пробела после последнего элемента в списке вы можете предварить его \lstinline!~^!.
\begin{myverb}
(format nil "~{~a~^, ~}" (list 1 2 3)) ==> "1, 2, 3"
\end{myverb}
После первых двух итераций, при обработке \lstinline!~^!, в списке остаются необработанные
элементы. При этом на третий раз, после того как директива \lstinline!~a! обработает
\lstinline{3}, \lstinline!~^! заставит \lstinline{FORMAT} прервать итерацию без печати запятой и
пробела.
С модификатором \lstinline!@! \lstinline!~{! обработает оставшийся аргумент формата как список.
\begin{myverb}
(format nil "~@{~a~^, ~}" 1 2 3) ==> "1, 2, 3"
\end{myverb}
Внутри тела \lstinline!~{...~}! специальный префиксный параметр \lstinline!#! ссылается на число необработанных элементов списка, а
не на число оставшихся элементов формата. Вы можете использовать его вместе с директивой \lstinline!~[! для печати
разделённого запятыми списка с <<and>> перед последним элементом, вот так:
\begin{myverb}
(format nil "~{~a~#[~;, and ~:;, ~]~}" (list 1 2 3)) ==> "1, 2, and 3"
\end{myverb}
Тем не менее этот подход совершенно не работает, когда список имеет длину в два элемента,
поскольку тогда добавляется лишняя запятая.
\begin{myverb}
(format nil "~{~a~#[~;, and ~:;, ~]~}" (list 1 2)) ==> "1, and 2"
\end{myverb}
Вы можете поправить это кучей способов. Следующий пользуется эффектом, который даёт директива \lstinline!~@{!, когда она
заключена внутри другой директивы \lstinline!~{! или \lstinline!~@{!~-- в этом случае она перебирает все элементы, оставшиеся в
обрабатываемом внешней директивой \lstinline!~{! списке. Вы можете объединить её с директивой \lstinline!~#[!, чтобы следующая
управляющая строка форматировала список в соответствии с английской грамматикой:
\begin{myverb}
(defparameter *english-list*
"~{~#[~;~a~;~a and ~a~:;~@{~a~#[~;, and ~:;, ~]~}~]~}")
(format nil *english-list* '()) ==> ""
(format nil *english-list* '(1)) ==> "1"
(format nil *english-list* '(1 2)) ==> "1 and 2"
(format nil *english-list* '(1 2 3)) ==> "1, 2, and 3"
(format nil *english-list* '(1 2 3 4)) ==> "1, 2, 3, and 4"
\end{myverb}
В~то время как эта управляющая строка приближается к такому классу кода, который трудно понять, после того как
он написан, все-таки его можно понять, если у вас есть немного времени. Внешние
\lstinline!~{...~}! принимают список и затем перебирают его. Все тело цикла состоит из \lstinline!~#[...~];!, печать
производится на каждом шаге цикла и таким образом зависит от количества обрабатываемых элементов, оставшихся в
списке. Разделяя директиву \lstinline!~#[...~]! разделителями выражений \lstinline!~;!, вы можете увидеть, что она состоит из
четырёх выражений, последнее из которых является выражением по умолчанию, поскольку его предваряет \lstinline!~:;!, в
отличие от простой \lstinline!~;!. Первое выражение, выполняющееся при нулевом числе элементов, пусто, оно нужно только
в том случае, если обрабатываемых элементов больше не осталось, тогда мы должны остановиться. Второе выражение с
помощью простой директивы \lstinline!~a! обрабатывает случай, когда элемент единственный. С двумя элементами справляется
<<\lstinline!~a and ~a!>>. И выражение по умолчанию, которое имеет дело с тремя и более элементами, состоит из другой
директивы итерации, в этот раз используется \lstinline!~@{! для перебора оставшихся элементов списка, обрабатываемых внешней
\lstinline!~{!. В~итоге тело цикла представляет собой управляющую строку, которая может корректно обработать список трёх
или более элементов, что в данной ситуации более чем достаточно. Поскольку цикл \lstinline!~@{! использует все
оставшиеся элементы списка, внешний цикл выполняется только один раз.
Если вы хотите напечатать что-нибудь особенное, например <<\lstinline!<empty>!>>, когда
список пуст, то у вас есть пара способов это сделать. Возможно, проще всего будет вставить
нужный вам текст в первое (нулевое) выражение внешней \lstinline!~#[! и затем добавить
модификатор \textit{двоеточие} к замыкающей \lstinline!~}! внешнего цикла~-- двоеточие
заставит цикл выполниться по меньшей мере один раз, даже если список пуст, и в этот момент
\lstinline{FORMAT} обработает нулевое выражение условной директивы.
\begin{myverb}
(defparameter *english-list*
"~{~#[<empty>~;~a~;~a and ~a~:;~@{~a~#[~;, and ~:;, ~]~}~]~:}")
(format nil *english-list* '()) ==> "<empty>"
\end{myverb}
Удивительно, что директива \lstinline!~{! предоставляет даже больше вариантов с различными комбинациями префиксных
параметров и модификаторов. Я не буду обсуждать их, только скажу, что вы можете использовать целочисленный
префиксный параметр для ограничения максимального числа выполнений итерации, и что с модификатором \textit{двоеточие}
каждый элемент списка (как настоящего, так и созданного директивой \lstinline!~@{!) сам должен быть списком, чьи
элементы затем будут использованы как аргументы управляющей строки в директиве \lstinline!~:{...~}!.
\section{Тройной прыжок}
Более простой директивой является \lstinline!~*!, которая позволяет вам перемещаться по
списку аргументов формата. В~своей основной форме, без модификаторов, она прос\-то
пропускает следующий аргумент, при этом ничего не печатается. Однако чаще она используется
с модификатором \textit{двоеточие}, который заставляет её отступить назад, позволяя одному
и тому же аргументу быть обработанным дважды. Например, вы можете использовать
\lstinline!~:*! для печати числового аргумента один раз словом, а другой раз цифрами, вот
так:
\begin{myverb}
(format nil "~r ~:*(~d)" 1) ==> "one (1)"
\end{myverb}
Еще вы можете реализовать директиву, похожую на \lstinline!~:P!, для неправильной формы множественного числа (в английском
языке), объединяя \lstinline!~:*! с~\lstinline!~[!.
\begin{myverb}
(format nil "I saw ~r el~:*~[ves~;f~:;ves~]." 0) ==> "I saw zero elves."
(format nil "I saw ~r el~:*~[ves~;f~:;ves~]." 1) ==> "I saw one elf."
(format nil "I saw ~r el~:*~[ves~;f~:;ves~]." 2) ==> "I saw two elves."
\end{myverb}
В~этой управляющей строке \lstinline!~R! печатает аргумент формата в виде количественного числительного. Затем директива
\lstinline!~:*! возвращается назад, так что число также используется как аргумент директивы \lstinline!~[!, выбирая между
выражениями, когда число равно нулю, единице или чему-нибудь ещё\footnote{Если вы находите фразу <<I saw zero elves>>
(<<Я видел нуль эльфов>>) немного неуклюжей, то можете использовать слегка усовершенствованную управляющую строку,
которая использует \lstinline!~:*! иначе, вот таким образом:
\begin{myverb}
(format nil "I saw ~[no~:;~:*~r~] el~:*~[ves~;f~:;ves~]." 0) ==> "I saw no elves."
(format nil "I saw ~[no~:;~:*~r~] el~:*~[ves~;f~:;ves~]." 1) ==> "I saw one elf."
(format nil "I saw ~[no~:;~:*~r~] el~:*~[ves~;f~:;ves~]." 2) ==> "I saw two elves."
\end{myverb}
}\hspace{\footnotenegspace}.
Внутри директивы \lstinline!~{! \lstinline!~*! пропускает или перескакивает назад через элементы списка. Например, вы можете
напечатать только ключи в списке свойств (plist), таким образом:
\begin{myverb}
(format nil "~{~s~*~^ ~}" '(:a 10 :b 20)) ==> ":A :B"
\end{myverb}
Директиве \lstinline!~*! также может быть задан префиксный параметр. Без модификаторов или
с модификатором \textit{двоеточие} этот параметр определяет число аргументов, на которое
нужно передвинуться вперёд или назад, и по умолчанию равняется единице. С~модификатором
\lstinline!@! префиксный параметр определяет абсолютный, отсчитываемый от нуля индекс
аргумента, на который нужно перемеситься, по умолчанию это нуль. Вариант \lstinline!~*! с
\lstinline!@! может быть полезен, если вы хотите использовать различные управляющие строки
для формирования различных сообщений для одних и тех же аргументов и если этим сообщениям
нужно использовать свои аргументы в различном порядке\footnote{Эта проблема может
возникнуть при попытке локализовать приложение и перевести удо\-бо\-чи\-тае\-мые сообщения на
различные языки. \lstinline{FORMAT} может помочь с некоторыми из этих проблем, но это далеко
на полноценная система локализации.}\hspace{\footnotenegspace}.
\section{И многое другое...}
Осталось ещё многое~-- я не упоминал о директиве \lstinline!~?!, которая может брать
фрагменты управляющих строк из аргументов формата, или директиве \lstinline!~/!, которая
позволяет вызвать произвольную функцию для обработки следующего аргумента формата. И ещё
остались все директивы для формирования табличной и удобочитаемой печати. Но уже
затронутых в этой главе директив пока будет вполне достаточно.
В~следующей главе вы перейдёте к системе условий и перезапусков Common 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.