An anonymous commenter says the tail-recursive loop in my previous post would be clearer with
loop. I agree in principle: recursion is more general than needed, so a loop should be clearer. Should be. But Common Lisp is not especially great at expressing iteration.
(loop for c = (peek-char nil stream eof-error-p) while (whitespacep c) do (read-char stream) finally (return c))
I'm one of those “knee-jerk anti-
loopists”, so maybe I'm reading this with unfriendly eyes, but it doesn't look any clearer to me. It saves a line of explicit recursion, but spends one to explicitly return the result (but maybe I just don't know how to use
loop; is there a better way?), which makes the control flow a little odd. Plus I don't like having to parse an S-expression!
What about ordinary
(do ((c (peek-char nil stream eof-error-p) (peek-char nil stream eof-error-p))) ((not (whitespacep c)) c) (read-char stream eof-error-p))
do has more non-form parentheses than I'd like, but it would be considerably shorter if it weren't for the hideous repetition of
(peek-char nil stream eof-error-p). This is an unfortunate side-effect of a feature (not stepping variables with no step-form) I rarely (never?) use - if I wanted a non-stepping variable, I'd use
let. In my opinion
do would be much more useful if it handled the common case instead, and made a two-element binding clause
(var step) equivalent to
(var step step). One could also reduce the mystery parentheses a bit (and increase generality) at the cost of only a little verbosity, by making the end-test a standalone form:
(defmacro until (test &body result-forms) "Terminate a loop if TEST evaluates to true." `(when ,test (return (progn ,@result-forms)))) (do ((c (peek-char nil stream eof-error-p))) (until (not (whitespacep c)) c) (read-char stream eof-error-p))
That's not so bad.
I'm not the first Common Lisper to complain about the lack of iteration facilities. Jonathan Amsterdam's answer is
iterate, which is basically
loop with parentheses and more features - including one,
finding, for writing this sort of loop. Here's the (untested)
(iter (for c next (peek-char nil stream eof-error-p)) (finding c such-that (not (whitespacep c))) (read-char stream eof-error-p))
It would be a two-liner, except that
(finding ... such-that (complement #'whitespacep)) doesn't work - that shortcut only works if the right side is a
(function ...) form.
That functional bit is a reminder that I'm going about this the wrong way. None of these loops approaches the clarity of the functional way. If the sequence functions worked on streams, reading characters as needed, we could just do something like:
(find-if (complement #'whitespacep) stream)
and unread the character afterward, if we got one. Or, if
peeking returns a derived stream which delays removing each character until the next one is needed:
(find-if (complement #'whitespacep) (peeking stream))
These functional conveniences are less general than
iterate, but that's okay. Their lack of generality makes the common loops much simpler. Too bad they don't exist.