It is often necessary to print strings containing the printed representations of Scheme objects, especially numbers. Doing so with Scheme's standard output routines can be tedious. For example, the tree-print procedure of Section 9.4 requires a sequence of four calls to output routines to print a simple one-line message:
(write (tree-count node) p) (write-char #\space p) (display (tree-word node) p) (newline p)
The formatted output facility defined in this section allows these four calls to be replaced by the single call to fprintf below.
(fprintf p "~s ~a~%" (tree-count node) (tree-word node))
fprintf expects a port argument, a control string, and an indefinite number of additional arguments that are inserted into the output as specified by the control string. In the example, the value of (tree-count node) is written first, in place of ~s. This is followed by a space and the displayed value of (tree-word node), in place of ~a. The ~% is replaced in the output with a newline.
The procedure printf, also defined in this section, is like fprintf except that no port argument is expected and output is sent to the current output port.
~s, ~a, and ~% are format directives; ~s causes the first unused argument after the control string to be printed to the output via write, ~a causes the first unused argument to be printed via display, and ~% simply causes a newline character to be printed. The simple implementation of fprintf below recognizes only one other format directive, ~~, which inserts a tilde into the output. For example,
(printf "The string ~s displays as ~~.~%" "~")
prints
The string "~" displays as ~. --------------------------------------------------------------------------- (let () ;; dofmt does all of the work. It loops through the control string ;;recognizing format directives and printing all other characters ;;without interpretation. A tilde at the end of a control string is ;;treated as an ordinary character. No checks are made for proper ;;inputs. Directives may be given in either lower or upper case. (define dofmt (lambda (p cntl args) (let ((nmax (- (string-length cntl) 1))) (let loop ((n 0) (a args)) (if (<= n nmax) (let ((c (string-ref cntl n))) (if (and (char=? c #\~) (< n nmax)) (case (string-ref cntl (+ n 1)) ((#\a #\A) (display (car a) p) (loop (+ n 2) (cdr a))) ((#\s #\S) (write (car a) p) (loop (+ n 2) (cdr a))) ((#\%) (newline p) (loop (+ n 2) a)) ((#\~) (write-char #\~ p) (loop (+ n 2) a)) (else (write-char c p) (loop (+ n 1) a))) (begin (write-char c p) (loop (+ n 1) a))))))))) ;; printf and fprintf differ only in that fprintf passes its ;;port argument to dofmt while printf passes the current output ;;port. (set! printf (lambda (control . args) (dofmt (current-output-port) control args))) (set! fprintf (lambda (p control . args) (dofmt p control args))))
Exercise 9.6.1.
Using the optional radix argument to number->string, augment printf and fprintf with support for the following new format directives:
~b or ~B: print the next unused argument, which must be a number, in binary;
~o or ~O: print the next unused argument, which must be a number, in octal; and
~x or ~X: print the next unused argument, which must be a number, in hexadecimal.
For example:
(printf "#x~x #o~o #b~b~%" 16 8 2)
would print
#x10 #o10 #b10
Exercise 9.6.2.
Add an "indirect" format directive, ~@, that treats the next unused argument, which must be a string, as if it were spliced into the current format string. For example:
(printf "--- ~@ ---" "> ~s <" '(a b c))
would print
---> (a b c) <---
Exercise 9.6.3.
Implement format, a version of fprintf that places its output into a string instead of writing to a port. Make use of object->string from Exercise 9.5.2 to support the ~s and ~a directives.
(let ((x 3) (y 4)) (format "~s + ~s = ~s" x y (+ x y))) ⇒ "3 + 4 = 7"
Exercise 9.6.4.
Modify format, fprintf, and printf to allow a field size to be specified after the tilde in the ~a and ~s format directives. For example, the directive ~10s would cause the next unused argument to be inserted into the output left- justified in a field of size 10. If the object requires more spaces than the amount specified, allow it to extend beyond the field.
(let ((x 'abc) (y '(def))) (format "(cons '~5s '~5s) = ~5s" x y (cons x y))) ⇒ "(cons 'abc '(def)) = (abc def)"
[Hint: Use format recursively.]