Регистрация | Войти
Lisp — программируемый язык программирования
Предыдущая Оглавление Следующая

18. Несколько рецептов для функции FORMAT

Функция FORMAT вместе с расширенным макросом LOOP - одна из двух возможностей Common Lisp, которые вызывают сильную эмоциональную реакцию у многих пользователей Common Lisp. Некоторые их любят, другие ненавидят 1)

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

(loop for cons on list
    do (format t "~a" (car cons))
    when (cdr cons) do (format t ", ")
)

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

(format t "~{~a~^, ~}" list)

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

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

Чтобы сильнее запутать дело, FORMAT поддерживает три совершенно разных вида форматирования: печать таблиц с данными, структурная печать (pretty printing) s-выражений, и формирование удобочитаемых сообщений со вставленными FIXME (interpolated? вложенными?) значениями. Печать таблиц с текстовыми данными на сегодня несколько устарела; это одно из напоминаний, что Lisp стар, как FORTRAN. В действительности, некоторые директивы, которые вы можете использовать для печати значений с плавающей точкой внутри полей с фиксированной длинной были основаны прямо на edit descriptors FIXME (дескрипторах редактирования?) FORTRAN, которые использовались в FORTRAN для чтения и печати столбцов с данными, расположенными внутри полей с фиксированной длинной. Тем не менее, использование Common Lisp как замены FORTRAN выходят за рамки этой книги, так что я не буду обсуждать эти аспекты FORMAT.

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

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

Функция FORMAT

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

Первым аргументом FORMAT, получателем для печатаемого текста, может быть T, NIL, поток, или строка с указателем заполнения. Т обозначает поток *STANDARD-OUTPUT*, в то время как NIL заставляет FORMAT сформировать свой вывод в виде строки, которую функция затем возвращает 4) Если получатель - поток, то вывод пишется в поток. А если получатель - строка с указателем заполнения, то форматированный вывод добавляется к концу строки и указатель заполнения соответственно выравнивается. За исключением случая, когда получатель - NIL и функция возвращает строку, FORMAT возвращает NIL.

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

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

Директивы FORMAT

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

CL-USER> (format t "~$" pi)
3.14
NIL

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

CL-USER> (format t "~5$" pi)
3.14159
NIL

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

CL-USER> (format t "~v$" 3 pi)
3.142
NIL
CL-USER> (format t "~#$" pi)
3.1
NIL

Я дам более правдоподобные примеры использования аргумента # в разделе "Условное форматирование".

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

CL-USER> (format t "~,5f" pi)
3.14159
NIL

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

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

В случае необходимости вы можете объединить модификаторы двоеточие и @, для того чтобы получить оба варианта:

CL-USER> (format t "~:@d" 1000000)
+1,000,000
NIL

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

Основы Форматирования

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

Наиболее универсальная директива - ~A, которая использует один аргумент формата любого типа и печатает его в эстетичной (удобочитаемой) форме. Например, строки печатаются без кавычек и экранирующих символов (escape characters), а числа печатаются в форме, принятой для соответствующего числового типа. Если вы хотите просто получить значение, предназначенное для прочтения человеком, то эта директива - ваш выбор.

(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)"

Родственная директива, ~S, также требует один аргумент формата любого типа, и печатает его. Однако ~S пытается сформировать такой вывод, который мог бы быть прочитан обратно с помощью READ. Поэтому, строки должны быть заключены в кавычки, при необходимости знаки должны быть пакетно-специлизированы FIXME (package-qualified?), и так далее. Объекты, которые не имеют подходящего для READ представления печатаются в нечитаемом синтаксисе объектов FIXME, #<>. С модификатором двоеточие, обе директивы ~A и ~S порождают NIL в виде (), а не как NIL. Обе директивы ~A и ~S также принимают до четырех префиксных параметров, которые могут быть использованы для выравнивания пробелами FIXME (padding?), добавляемыми после (или до, при модификаторе @) значения, впрочем эти функции действительно полезны лишь при формировании табличных данных.

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

Реже используется родственная им директива ~~, которая заставляет FORMAT вывести знак тильды. Подобно директивам ~% и ~&, она может быть параметризована числом, которое задает количество выводимых тильд.

Знаковые и Целочисленные директивы

Вдобавок к директивам общего назначения, ~A и ~S, FORMAT поддерживает несколько директив, которые могут использоваться для получения значений определенных типов особыми способами. Простейшая из них является директива ~C, которая используется для вывода знаков. Ей не требуются префиксные аргументы, но ее работу можно корректировать с помощью модификаторов двоеточие и @. Без модификаций ее поведение на отличается от поведения ~A, за исключением того, что она работает только со знаками. Модифицированные версии более полезны. С модификатором двоеточие, ~:C выводит заданные по имени непечатаемые знаки, такие как пробел, символ табуляции и перевод строки. Это полезно, если вы хотите создать сообщение к пользователю, в котором упоминается некоторый символ. Например, следующий код:

(format t "Syntax error. Unexpected character: ~:c" char)

может напечатать такое сообщения:

Syntax error. Unexpected character: a

а еще вот такое:

Syntax error. Unexpected character: Space

Вместе с модификатором @, ~@С выведет знак в синтаксисе знаков Lisp:

CL-USER> (format t "~@c~%" #\a)
#\a
NIL

Одновременно с модификаторами двоеточие и @, директива ~C может напечатать дополнительную информацию о том, как ввести символ с клавиатуры, если для этого требуется специальная клавиатурная комбинация. Например, на Macintosh, в некоторых приложениях вы можете ввести нулевой символ (код символа 0 в ASCII или в любом надмножестве ASCII, наподобие ISO-8859-1 или Unicode) удерживая клавиши Control и нажав '@'. В OpenMCL, если вы напечатаете нулевой символ c помощью директивы ~:C, то она сообщит вам следующее:

(format nil "~:@c" (code-char 0)) ==> "^@ (Control @)"

Однако не все версии Lisp реализуют этот аспект директивы ~C. Даже если они это делают, то реализация может и не быть аккуратной - например, если вы запустите OpenMCL в SLIME, комбинация клавиш C-@ перехватывается Emacs, вызывая команду set-mark-command5)

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

Вот пять родственных директив для форматированного вывода целых чисел: ~D, ~X, ~O, ~B, и ~R. Чаще всего применяется директива ~D, которая выводит целые числа по основанию 10.

(format nil "~d" 1000000) ==> "1000000"

Как я упоминал ранее, с модификатором двоеточие она добавляет запятые.

(format nil "~:d" 1000000) ==> "1,000,000"

А с модификатором @, она всегда будет печатать знак.

(format nil "~@d" 1000000) ==> "+1000000"

И оба модификатора могут быть объединены.

(format nil "~:@d" 1000000) ==> "+1,000,000"

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

(format nil "~12d" 1000000)    ==> "     1000000"
(format nil "~12,'0d" 1000000) ==> "000001000000"

Эти параметры удобны для форматирования объектов наподобие календарных дат в формате с фиксированной длинной.

(format nil "~4,'0d-~2,'0d-~2,'0d" 2005 6 10) ==> "2005-06-10"

Третий и четвертый параметры используются в связке с модификатором двоеточие: третий параметр определяет знак, используемый в качестве разделителя между группами и разрядами, а четвертый параметр определяет число разрядов в группе. Их значения по умолчанию - запятая и число 3 соответственно. Таким образом, вы можете использовать директиву ~:D без параметров для вывода больших чисел в стандартном для Соединенных Штатов формате, но можете заменить запятую на точку и группировку с 3 на 4 с помощью ~,,'.,4D.

(format nil "~:d" 100000000)       ==> "100,000,000"
(format nil "~,,'.,4:d" 100000000) ==> "1.0000.0000"

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

Директивы ~X, ~O, и ~B работают подобно директиве ~D, за исключением того, что они выводят числа в шестнадцатеричном, восьмеричном, и двоичном виде.

(format nil "~x" 1000000) ==> "f4240"
(format nil "~o" 1000000) ==> "3641100"
(format nil "~b" 1000000) ==> "11110100001001000000"

Наконец, директива ~R - универсальная директива для задания системы счисления. Ее первый параметр - число между 2 и 36 (включительно), которое обозначает, какое основание системы счисления использовать. Оставшиеся параметры такие же, что и четыре параметра, принимаемые директивами ~D, ~X, ~O, и ~B, а модификаторы двоеточие и @ меняют ее поведение схожим образом. Кроме того, директива ~R ведет себя особым образом при использовании без префиксных параметров, который я буду обсуждать в разделе "Директивы для Английского языка".

Директивы для Чисел с Плавающей Точкой

Вот четыре директивы для форматирования значений с плавающей точкой: ~F, ~E, ~G и ~$. Первые три из них - это директивы, основанные на дескрипторах редактирования FIXME (edit descriptor?) FORTRAN. Я пропущу большинство деталей этих директив, поскольку они в основном имеют дело с форматированием чисел с плавающей точкой для использования в табличной форме. Тем не менее вы можете использовать директивы ~F, ~E, и ~$ для вставки значений с плавающей точкой в текст. С другой стороны директива ~G, или генерал, FIXME (генерал?) сочетает аспекты директив ~F и ~E единственным осмысленным способом для печати таблиц.

Директива ~F печатает свой аргумент, который должен быть числом 6), в десятичном формате, по возможности контролируя количество разрядов после десятичной точки. Директиве ~F, тем не менее, разрешается использовать компьютеризированную научную нотацию FIXME (компьютеризированное экспоненциальное представление?), если число достаточно велико либо мало. Директива ~E, с другой стороны, всегда выводит числа в компьютеризированной научной нотации. Обе эти директивы принимают несколько префиксных параметров, но вам нужно беспокоиться только о втором, который управляет количеством разрядов, печатаемых после десятичной точки.

(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"

Директива ~$ (значок доллара) похожа на ~F, но несколько проще. Как подсказывает ее имя, она предназначена для вывода денежных единиц. Без параметров она эквивалентна ~,2F. Чтобы изменить количество разрядов, печатаемых после десятичной точки, используйте первый параметр, в то время как второй параметр регулирует минимальное количество разрядов, печатающихся до десятичной дочки.

(format nil "~$" pi) ==> "3.14"
(format nil "~2,4$" pi) ==> "0003.14"

С модификатором @ все три директивы, ~F, ~E и ~$ можно заставить всегда печатать знак, плюс или минус 7).

Директивы для Английского языка

Некоторые из удобнейших директив FORMAT для формирования удобочитаемых сообщений - те, которые выводят английский текст. Эти директивы позволяют вам выводить числа как английский текст, выводить слова в множественном числе в зависимости от значения аргумента формата, и менять регистр FIXME (case conversion? выполнять преобразование между строчными и прописными буквами?) в секциях вывода FORMAT.

Директива ~R, которую я обсуждал в разделе "Знаковые и Целочисленные директивы", при использовании без указания системы счисления, печатает числа как английские слова или римские цифры. При использовании без префиксного параметра и модификоторов, она выводит число словами как количественное числительное.

(format nil "~r" 1234) ==> "one thousand two hundred thirty-four"

С модификатором двоеточие она выводит число как порядковое числительное.

(format nil "~:r" 1234) ==> "one thousand two hundred thirty-fourth"

И вместе с модификатором @, она выводит число в виде римских цифр; вместе с @ и двоеточием она выводит римские цифры, в которых четверки и девятки записаны как IIII и VIIII вместо IV и IX.

(format nil "~@r" 1234)  ==> "MCCXXXIV"
(format nil "~:@r" 1234) ==> "MCCXXXIIII"

Для чисел, слишком больших, чтобы быть представленными в заданной форме, ~R ведет себя как ~D.

Чтобы помочь вам формировать сообщений со словами в нужном числе, FORMAT предоставляет директиву ~P, которая просто выводит 's' если соответствующий аргумент не 1.

(format nil "file~p" 1)  ==> "file"
(format nil "file~p" 10) ==> "files"
(format nil "file~p" 0) ==> "files"

Тем не меннее обычно вы будете использовать ~P вместе с модификатором двоеточие, который заставляет ее повторно обработать предыдущий аргумент формата.

(format nil "~r file~:p" 1)  ==> "one file"
(format nil "~r file~:p" 10) ==> "ten files"
(format nil "~r file~:p" 0) ==> "zero files"

С модификатором @, который может быть объединен с модификатором двоеточие, ~P выводит y или ies.

(format nil "~r famil~:@p" 1)  ==> "one family"
(format nil "~r famil~:@p" 10) ==> "ten families"
(format nil "~r famil~:@p" 0) ==> "zero families"

Очевидно, что ~P не может разрешить все проблемы образования множественного числа и не может помочь при формировании сообщений на других языках (отличных от английского), она удобна в тех ситуациях, для которых предназначена. А директива ~[, о которой я расскажу очень скоро, предоставит вам более гибкий способ параметризации вывода FORMAT.

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

(format nil "~(~a~)" "FOO") ==> "foo"
(format nil "~(~@r~)" 124) ==> "cxxiv

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

(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"

Условное Форматирование

Вдобавок к директивам, вставляющим в выводимый текст свои аргументы и видоизменяющими прочий вывод, FORMAT предоставляет несколько директив, который реализуют простые управляющие структуры внутри управляющих строк. Одна из них, которую вы использовали в главе 9, это условная директива ~[. Эта директива замыкается соответствующей директивой ~], а между ними находятся выражения, разделенные ~;. Работа директивы ~[ - выбрать одно из выражений, которое затем обрабатывается в FORMAT. Без модификаторов или параметров, выражение выбирается по числовому индексу; директива ~[ использует аргумент формата, который должен быть числом, и выбирает N-ное (считая от нуля) выражение, где N - значение аргумента.

(format nil "~[cero~;uno~;dos~]" 0) ==> "cero"
(format nil "~[cero~;uno~;dos~]" 1) ==> "uno"
(format nil "~[cero~;uno~;dos~]" 2) ==> "dos"

Если значение аргумента больше ,чем число выражений, то ничего не печатается.

(format nil "~[cero~;uno~;dos~]" 3) ==> ""

Однако если последний разделитель выражений это ~:; вместо ~;, тогда последнее выражение служит выражением по умолчанию.

(format nil "~[cero~;uno~;dos~:;mucho~]" 3)   ==> "mucho"
(format nil "~[cero~;uno~;dos~:;mucho~]" 100) ==> "mucho"

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

(defparameter *list-etc*
  "~#[NONE~;~a~;~a and ~a~:;~a, ~a~]~#[~; and ~a~:;, ~a, etc~]."
)

и использовать ее так:

(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."

Заметим, что управляющая строка в действительности содержит две ~[~] директивы - обе из которых применяют # для выбора используемого выражения. Первая директива использует от нуля до двух аргументов, тогда как вторая использует еще один, если он есть. FORMAT молча проигнорирует любые аргументы, сверх использованных во время обработки управляющей строки.

С модификатором двоеточие ~[ может содержать только два выражения; директива использует единственный аргумент и обрабатывает первое выражение, если аргумент NIL, и второе выражение в противном случае. Вы уже использовали этот вариант ~[ в главе 9 для формирования сообщений типа принять/отклонить FIXME (pass/fail message? сработало или не сработало?), таких как это:

(format t "~:[FAIL~;pass~]" test-result)

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

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

(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) ==> ""

Итерация

Другая директива FORMAT, мимоходом виденная вами, это директива итерации ~{. Эта директива сообщает FORMAT перебрать элементы списка или неявного списка аргументов формата.

Без модификаторов, ~{ принимает один аргумент формата, который должен являться списком. Подобно директиве ~[, которой всегда соответствует директива ~], директива ~{ всегда имеет соответствующую замыкающую ~}. Текст между двумя маркерами обрабатывается как управляющая строка, которая выбирает свой аргумент из списка, поглощенного FIXME (consumed?) директивой ~{. FORMAT будет циклически обрабатывать эту управляющую строку до тех пор, пока в перебираемом списке не останется элементов. В следующем примере, ~{ принимает один аргумент формата, список (1 2 3), и затем обрабатывает управляющую строку "~a, ", повторяя, пока все элементы списка не будут использованы.

(format nil "~{~a, ~}" (list 1 2 3)) ==> "1, 2, 3, "

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

(format nil "~{~a~^, ~}" (list 1 2 3)) ==> "1, 2, 3"

После первых двух итераций, при обработке ~^, в списке остаются необработанные элементы. При этом на третий раз, после того как директива ~a обработает 3, ~^ заставит FORMAT прервать итерацию без печати запятой и пробела.

С модификатором @, ~{ обработает оставшийся аргумент формата как список.

(format nil "~@{~a~^, ~}" 1 2 3) ==> "1, 2, 3"

Внутри тела ~{...~} специальный префиксный параметр # ссылается на число необработанных элементов списка, а не на число оставшихся элементов формата. Вы можете использовать его вместе с директивой ~[ для печати разделенного запятыми списка с "and" перед последним элементом, вот так:

(format nil "~{~a~#[~;, and ~:;, ~]~}" (list 1 2 3)) ==> "1, 2, and 3"

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

(format nil "~{~a~#[~;, and ~:;, ~]~}" (list 1 2)) ==> "1, and 2"

Вы можете поправить это кучей способов. Следующий пользуется эффектом, который дает директива ~@{, когда она заключена внутри другой директивы ~{ или ~@{ - в этом случае она перебирает все элементы, оставшиеся в обрабатываемом внешней директивой ~{ списке. Вы можете объединить ее с директивой ~#[ чтобы следующая управляющая строка форматировала список в соответствии с английской грамматикой:

(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"

В то время, как эта управляющая строка приближается к такому классу кода, который трудно понять, после того как он написан FIXME (write-only code?), все-таки это возможно, когда у вас есть немного времени. Внешние ~{...~} принимают список и затем перебирают его. Затем все тело цикла состоит из ~#[...~]; печать производится на каждом шаге цикла, и таким образом зависит от количества обрабатываемых элементов, оставшихся в списке. Разделяя директиву ~#[...~] разделителями выражений ~; вы можете увидеть, что она состоит из четырех выражений, последнее из которых является выражением по умолчанию, поскольку его предваряет ~:; в отличие от простой ~;. Первое выражение, выполняющееся при нулевом числе элементов, пусто, оно нужно только в том случае, если обрабатываемых элементов больше не осталось, тогда мы должны остановиться. Второе выражение с помощью простой директивы ~a обрабатывает случай, когда элемент единственный. С двумя элементами справляется "~a and ~a". И выражение по умолчанию, которое имеет дело с тремя и более элементами, состоит из другой директивы итерации, в этот раз используется ~@{ для перебора оставшихся элементов списка, обрабатываемых внешней ~{. В итоге тело цикла представляет собой управляющую строку, которая может корректно обработать список трех или более элементов, что в данной ситуации более чем достаточно. Поскольку цикл ~@{ использует все оставшиеся элементы списка, внешний цикл выполняется только один раз.

Если вы хотите напечатать что-нибудь особенное, например "<empty>", когда список пуст, то у вас есть пара способов это сделать. Возможно проще всего будет вставить нужный вам текст в первое (нулевое) выражение внешней ~#[ и затем добавить модификатор двоеточие к замыкающей ~} внешнего цикла - двоеточие заставит цикл выполниться по меньшей мере один раз, даже если список пуст, и в этот момент FORMAT обработает нулевое выражение условной директивы.

(defparameter *english-list*
"~{~#[<empty>~;~a~;~a and ~a~:;~@{~a~#[~;, and ~:;, ~]~}~]~:}")
(format nil *english-list* '()) ==> "<empty>"

Удивительно, что директива ~{ предоставляет даже больше вариантов с различными комбинациями префиксных параметров и модификаторов. Я не буду обсуждать их, только скажу, что вы можете использовать целочисленный префиксный параметр для ограничения максимального числа выполнений итерации, и что с модификатором двоеточие каждый элемент списка (как настоящего, так и созданного директивой ~@{) сам должен быть списком, чьи элементы затем будут использованы как аргументы управляющей строки в директиве ~:{...~}.

Тройной прыжок

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

(format nil "~r ~:*(~d)" 1) ==> "one (1)"

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

(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."

В этой управляющей строке ~R печатает аргумент формата в виде количественного числительного. Затем директива ~:* возвращается назад, так что число также используется как аргумент директивы ~[, выбирая между выражениями, когда число равно нулю, единице, или чему-нибудь еще 8)

Внутри директивы ~{, ~* пропускает или перескакивает назад через элементы списка. Например, вы можете напечатать только ключи в списке свойств FIXME (использовать термин "список свойств" или plist?), таким образом:

(format nil "~{~s~*~^ ~}" '(:a 10 :b 20)) ==> ":A :B"

Директиве ~* также может быть задан префиксный параметр. Без модификаторов или с модификатором двоеточие, этот параметр определяет число аргументов, на которое нужно передвинуться вперед или назад, и по умолчанию равняется единице. С модификатором @, префиксный параметр определяет абсолютный, отсчитываемый от нуля индекс аргумента, на который нужно перемеситься, по умолчанию это нуль. Вариант ~* с @ может быть полезен, если вы хотите использовать различные управляющие строки для формирования различных сообщений для одних и тех же аргументов, и если этим сообщениям нужно использовать свои аргументы в различном порядке 9)

И многое другое...

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

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

1)Конечно, большинство людей осознают, что не стоит возбуждаться по поводу чего-либо в языке программирования, и используют или не используют их без сильного беспокойства. С другой стороны, интересно, что две эти возможности - единственные в Common Lisp, которые реализуют по сути языки предметной области, используя синтаксис, не основанный на s-выражениях. Синтаксис управляющих строк FORMAT основан на символах, в то время как расширенный макрос LOOP может быть понят только в терминах грамматики ключевых слов LOOP. Одна из обычных придирок к FORMAT и LOOP - то, что они "не достаточно лисповские" - является доказательством того, что программистам на Lisp FIXME (Lispers - лисперы?) действительно нравиться синтаксис, основанный на s-выражениях.
2)Примечание переводчика: В оригинале - line noise, шум в линии, имеется ввиду схожесть строки формата с сигналом на осциллографе ~^~%~—
3)Читатели, интересующиеся в механизом структурной печати, могут почитать статью "XP: A Common Lisp Pretty Printing System" Ричарда Уотерса (Richard Waters). Это описание системы структурированной печати, которая в итоге была включена в Common Lisp. Вы можете найти загрузить ее с ftp://publications.ai.mit.edu/ai-publications/pdf/AIM-1102a.pdf.
4)Чтобы немного запутать дело, большинство остальных функций ввода/вывода также принимают T и NIL как указатели потоков, но с небольшим отличием: как указатель потока, Т обозначает двунаправленный поток *TERMINAL-IO*, тогда как NIL указывает на *STANDARD-OUTPUT* как на стандартный поток вывода и *STANDARD-INPUT* как на стандартный поток ввода.
5)Этот вариант директивы ~C имеет большее значение на платформах наподобие Lisp-машин, где нажатие клавиши представляется знаками Lisp.
6)Технически, если аргумент не является вещественным числом, предполагается, что ~F форматирует его так же, как это сделала бы директива ~D, которая ведет себя как директива ~A, если аргумент не является числом, но не все реализации ведут себя должным образом
7)Итак, вот что говорит стандарт языка. По какой-то причине, возможно коренящейся в общей унаследованной кодовой базе, некоторые реализации Common Lisp реализуют этот аспект директивы ~F некорректно
8)Если вы находите фразу "I saw zero elves" ("Я видел нуль эльфов") немного неуклюжей, то можете использовать слегка усовершенствованную управляющую строку, которая использует ~:* иначе, вот таким образом:
(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."
9)Эта проблема может возникнуть при попытке локализовать приложение и перевести удобочитаемые сообщения на различные языки. FORMAT может помочь с некоторыми из этих проблем, но это далеко на полноценная система локализации.
Предыдущая Оглавление Следующая
@2009-2013 lisper.ru