LOOPfacility, some don't. I used to fall into the first camp, for various reasons, the most important of which is that
LOOPis effectively a specialized looping language grafted onto lisp. Normally I'm a big fan of a single syntactic mode for all corners of a language (like the lisp family, or Smalltalk, quite unlike C or, god help us, perl).
I've been working on some basic forecasting and time series code recently, and I have to say, when you're looping over different time series and smoothing windows,
LOOPresults in neater code than almost any language I can think of. Here's a simple moving average forecast (in the interest of space, all examples have my anal-retentive sanity checking assertions removed):
(defmethod single-moving-average ((data sequence) (order integer))
(let ((n (length data)))
(/ (reduce #'+ data :start (- n order))
For such simple sums, the functional style
reducedoes the job. Once you get to a weighted moving average the math starts to get tricker. As I was thinking about the many, many traversals of sequences I'd be doing, I decided to check out
LOOPmore seriously by reading a chapter from Seibel's book I had previously skipped, 22.
LOOPfor Black Belts. I started to develop warm feelings for
LOOPimmediately. For starters, it does a great job of encapsulating the various sorts of set-up and tear-down you have to do when rolling your own loop so the mechanical bits for doing loops don't infest the rest of your code.
One really lovely touch makes it easy to avoid the off-by-one error — and fussing about — that comes when you use zero-indexed arrays. The
FORclause may indicate exclusive or inclusive bounds, with
BELOW ngoing up to but not including it. So here's a simple weighted moving average function:
(defmethod weighted-moving-average ((data sequence) (order integer))
(let ((n (length data)))
(/ (loop for i from 0 below n
sum (* (elt data i) (+ i 1)))
(/ (* n (+ n 1)) 2.0))))
Now I didn't really have to use
LOOPfor this, but the code I think is somewhat cleaner. The
SUMclause accumulates by summing successive values of the expression after it, and in this simple
LOOPclause that final sum will be the value of the expression.
My biggest example of
LOOP-fu this weekend is a weighted moving average smoothing function. It takes a sequence of data and a sequence of weights and spits out a vector of the smoothed data. In this implementation I simply take the original values at the edges of the data where the smoothing sequence is longer than available values. What I need to do at each step is apply the weight vector to a window of data to compute the moving average for that step. This brings out the other really lovely feature of
LOOP: parallel loop values. Here's the scary result, somewhat un-lisp-like to my eyes, but clearer I suspect than I'd be able to produce with functional style tools and
(defmethod weighted-average ((data sequence) (weights sequence))
(let ((d-n (length data))
(w-n (length weights)))
(loop with smoothed = (make-array (list d-n))
with start = (- w-n 1)
with end = (- d-n w-n)
with denom = (reduce #'+ weights)
for i from 0 below d-n
if (or (< i start) (> i end))
do (setf (aref smoothed i) (elt data i))
do (setf (aref smoothed i)
(/ (loop for j from 0 below w-n
for dj from (- i start) to i
summing (* (elt weights j) (elt data dj)))
finally (return smoothed))))
LOOP! The underlined section shows the parallel loop indices,
jgoing over the weights sequence and
djgoing over the current window on the data. In the outer
LOOPI went a bit crazy and used a lot of its abilities — initializing temporary variables,
FINALLYclause — with the results that look like an Algol-Lisp chimera.
If I were a code purist
weighted-averagewould probably make me crazy. Good thing I'm not.