LAMBDA: The Ultimate Formatter (was Re: Format strings are wrong)
Taylor Campbell 28 Dec 2003 07:17 UTC
;; Requires some non-R5RS utilities:
;; - (CALL-WITH-OUTPUT-STRING <consumer>) -> string
;; (CONSUMER <output-port>)
;; - (ERROR <message> <irritant> ...)
;; Used in one place to signal a 'too many arguments' error.
;; - (ASCII-LIMIT) -> exact, nonnegative integer
;; The maximum number of ASCII characters.
;; - (CHAR->ASCII <char>) -> exact, nonnegative integer
;; The ASCII numeric value for CHAR.
;; (CHAR->INTEGER doesn't mandate ASCII.)
(define (format formatter write-char)
(formatter write-char))
(define (format/string formatter)
(call-with-output-string
(lambda (out)
(format formatter (lambda (c) (write-char c out))))))
(define (format/port formatter . maybe-out)
(format formatter
(cond ((null? maybe-out)
write-char)
((null? (cdr maybe-out))
(let ((out (car maybe-out)))
(lambda (c) (write-char c out))))
(else
(error "Too many arguments"
(cons format/port (cons formatter maybe-out)))))))
(define (format/char-list formatter)
(let ((l '()))
(format formatter (lambda (c) (set! l (cons c l))))
(reverse l)))
(define (sequence-formatter . formatters)
(lambda (write-char)
(for-each (lambda (f) (format f write-char)) formatters)))
(define (string-formatter string)
(let ((len (string-length string)))
(lambda (write-char)
(do ((i 0 (+ i 1))) ((= i len))
(write-char (string-ref string i))))))
(define (string-literal-formatter string)
(let ((f (string-formatter string)))
(lambda (write-char)
(write-char #\")
(format f write-char)
(write-char #\"))))
(define (symbol-formatter symbol)
(string-formatter (symbol->string symbol)))
;; This is just a simple example using NUMBER->STRING; there should, of
course,
;; be real numeric formatting routines with a lot more power than just
over the
;; radix.
(define (number-formatter number radix)
(string-formatter (number->string number radix)))
(define (boolean-formatter boolean)
(string-formatter (if boolean "#t" "#f")))
(define (char-formatter char)
(lambda (write-char)
(write-char char)))
(define (char-literal-formatter char)
(let ((f (cond ((char-name char)
=> (lambda (x)
((if (string? x) string-formatter
symbol-formatter)
x)))
(else
(char-formatter char)))))
(lambda (write-char)
(write-char #\#)
(write-char #\\)
(format f write-char))))
;; Make the named characters print more nicely.
(define *char-names* (make-vector (ascii-limit) #f))
(define (char-name char)
(vector-ref *char-names* (char->ascii char)))
(define (define-char-name char name)
(vector-set! *char-names* (char->ascii char) name))
(define-char-name #\space 'space)
(define-char-name #\newline 'newline)
;; Does R5RS define any other named characters?
;; Handles proper & dotted lists, but not circular lists.
(define (list-formatter l x->formatter)
(cond ((null? l)
;; Empty proper list case
(string-formatter "()"))
((not (pair? l))
;; Empty dotted list case
(x->formatter l))
(else
;; Nonempty either list kind case
(apply sequence-formatter (build-formatter-list l
x->formatter)))))
(define (build-formatter-list l x->formatter)
(cons (char-formatter #\()
(cons (x->formatter (car l))
(let recur ((l (cdr l)))
(cond ((null? l)
(list (char-formatter #\))))
((not (pair? l))
(list (string-formatter " . ")
(x->formatter l)
(char-formatter #\))))
(else
(cons (char-formatter #\space)
(cons (x->formatter (car l))
(recur (cdr l))))))))))
(define (vector-formatter vec x->formatter)
;; This is a cheat. A real implementation would use a
straightforward vector
;; element formatter.
(sequence-formatter (char-formatter #\#)
(list-formatter (vector->list vec) x->formatter)))
;; These two could be a bit more efficient if the formatting library
were to be
;; integrated with the implementations of DISPLAY & WRITE, by not
allocating an
;; intermediate string and generating the formatter for that.
(define (write-formatter object)
(string-formatter (call-with-output-string
(lambda (out) (write object out)))))
(define (display-formatter object)
(string-formatter (call-with-output-string
(lambda (out) (display object out)))))