Книга: Practical Common Lisp

Iteration

Iteration

Another FORMAT directive that you've seen already, in passing, is the iteration directive ~{. This directive tells FORMAT to iterate over the elements of a list or over the implicit list of the format arguments.

With no modifiers, ~{ consumes one format argument, which must be a list. Like the ~[ directive, which is always paired with a ~] directive, the ~{ directive is always paired with a closing ~}. The text between the two markers is processed as a control string, which draws its arguments from the list consumed by the ~{ directive. FORMAT will repeatedly process this control string for as long as the list being iterated over has elements left. In the following example, the ~{ consumes the single format argument, the list (1 2 3), and then processes the control string "~a, ", repeating until all the elements of the list have been consumed.

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

However, it's annoying that in the output the last element of the list is followed by a comma and a space. You can fix that with the ~^ directive; within the body of a ~{ directive, the ~^ causes the iteration to stop immediately, without processing the rest of the control string, when no elements remain in the list. Thus, to avoid printing the comma and space after the last element of a list, you can precede them with a ~^.

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

The first two times through the iteration, there are still unprocessed elements in the list when the ~^ is processed. The third time through, however, after the ~a directive consumes the 3, the ~^ will cause FORMAT to break out of the iteration without printing the comma and space.

With an at-sign modifier, ~{ processes the remaining format arguments as a list.

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

Within the body of a ~{...~}, the special prefix parameter # refers to the number of items remaining to be processed in the list rather than the number of remaining format arguments. You can use that, along with the ~[ directive, to print a comma-separated list with an "and" before the last item like this:

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

However, that doesn't really work right if the list is two items long because it adds an extra comma.

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

You could fix that in a bunch of ways. The following takes advantage of the behavior of ~@{ when nested inside another ~{ or ~@{ directive—it iterates over whatever items remain in the list being iterated over by the outer ~{. You can combine that with a ~#[ directive to make the following control string for formatting lists according to English grammar:

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

While that control string verges on being "write-only" code, it's not too hard to understand if you take it a bit at a time. The outer ~{...~} will consume and iterate over a list. The whole body of the iteration then consists of a ~#[...~]; the output generated each time through the iteration will thus depend on the number of items left to be processed from the list. Splitting apart the ~#[...~] directive on the ~; clause separators, you can see that it's made up of four clauses, the last of which is a default clause because it's preceded by a ~:; rather than a plain ~;. The first clause, for when there are zero elements to be processed, is empty, which makes sense—if there are no more elements to be processed, the iteration would've stopped already. The second clause handles the case of one element with a simple ~a directive. Two elements are handled with "~a and ~a". And the default clause, which handles three or more elements, consists of another iteration directive, this time using ~@{ to iterate over the remaining elements of the list being processed by the outer ~{. And the body of that iteration is the control string that can handle a list of three or more elements correctly, which is fine in this context. Because the ~@{ loop consumes all the remaining list items, the outer loop iterates only once.

If you wanted to print something special such as "<empty>" when the list was empty, you have a couple ways to do it. Perhaps the easiest is to put the text you want into the first (zeroth) clause of the outer ~#[ and then add a colon modifier to the closing ~} of the outer iteration—the colon forces the iteration to be run at least once, even if the list is empty, at which point FORMAT processes the zeroth clause of the conditional directive.

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

Amazingly, the ~{ directive provides even more variations with different combinations of prefix parameters and modifiers. I won't discuss them other than to say you can use an integer prefix parameter to limit the maximum number of iterations and that, with a colon modifier, each element of the list (either an actual list or the list constructed by the ~@{ directive) must itself be a list whose elements will then be used as arguments to the control string in the ~:{...~} directive.

Оглавление книги


Генерация: 0.750. Запросов К БД/Cache: 2 / 0
поделиться
Вверх Вниз