The Common Lisp equivalent of scanf().

A (relatively) frequently asked question on comp.lang.lisp is "What's the equivalent to scanf()?". The usual answer is "There isn't one, because it's too hard to work out what should happen". Which is fair enough.

However, one year Christophe was bored during exams, so he wrote format-setf.lisp, which may do what you want.

It should be pointed out that currently the behaviour of this program is unspecified, in more senses than just the clobbering of symbols in the "CL" package. What would be nice would be to see a specification appear for its behaviour, so that I don't have an excuse when people say that it's buggy.

Quick assessment (2023-03-24)

For example:

(setf (format nil "~F, ~D" x y) "2.2, 3")

correctly parses the string into x and y, whereas

(setf (format nil "~D, ~D" x y) "2.2, 3")

fails because we used the wrong format directive.

Portability notes (2024-01-21)

There's a comment in the code asking why the definition of whitespacep (from has an eval-when around it. The reason is that some Lisps—CCL, at least—already have a function of that name, so there's an (unless (fboundp 'whitespacep) ...) around it. Because of this, the defining form is not a top level form. The equivalent function in the parse-float system is called whitespace-char-p.

An observation about the last line of the file—the defsetf associated with format-setf—is that it assumes the short-form defsetf will provide the simplest possible expansion. This is because format-setf is a macro that expects to receive literal arguments without evaluating them. In LispWorks, for example, macroexpand-1 gives:


But npt, on the other hand, does a lot of renaming to gensyms:

(PROGN (LET* ((#:G131 NIL) (#:G132 "~F, ~D") (#:G133 X) (#:G134 Y) (#:G "2.2, 3")) (DECLARE (IGNORABLE #:G131 #:G132 #:G133 #:G134)) (FORMAT-SETF::FORMAT-SETF #:G131 #:G132 #:G133 #:G134 #:G)))

Which won't work. The workaround is to get rid of the defsetf and manage the expansion ourselves:

(define-setf-expander format (stream control-string &rest arguments &environment env) (loop with temps with vals with stores with store-form with access-form for place in arguments do (setf (values temps vals stores store-form access-form) (get-setf-expansion place env)) append temps into combined-temps append vals into combined-vals append stores into combined-stores collect store-form into combined-store-forms collect access-form into combined-access-forms finally (let ((store (gensym "FORMAT-STORE"))) (return (values combined-temps combined-vals (list* store combined-stores) `(prog1 (format-setf::format-setf ,stream ,control-string ,@combined-stores ,store) ,@combined-store-forms) `(format ,stream ,control-string ,@combined-access-forms))))))

So the result from get-setf-expansion is:

* (get-setf-expansion '(format nil "~F, ~D" x y)) NIL NIL (#:FORMAT-STORE174 #:G172 #:G173) (PROG1 (FORMAT-SETF::FORMAT-SETF NIL "~F, ~D" #:G172 #:G173 #:FORMAT-STORE174) (SETQ X #:G172) (SETQ Y #:G173)) (FORMAT NIL "~F, ~D" X Y)

The expander looks long-winded, but all it's doing is combining the setf expansions of its arguments.