LOOP
facility, some don't. I used to fall into the first camp, for various reasons, the most important of which is that LOOP
is 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,
LOOP
results 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))
order)))
For such simple sums, the functional style
reduce
does 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 LOOP
more seriously by reading a chapter from Seibel's book I had previously skipped, 22. LOOP
for Black Belts. I started to develop warm feelings for LOOP
immediately. 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
FOR
clause may indicate exclusive or inclusive bounds, with TO n
including n
, and BELOW n
going 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
LOOP
for this, but the code I think is somewhat cleaner. The SUM
clause accumulates by summing successive values of the expression after it, and in this simple LOOP
clause 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 DO
:(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))
else
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)))
denom))
finally (return smoothed))))
A
LOOP
within a LOOP
! The underlined section shows the parallel loop indices, j
going over the weights sequence and dj
going over the current window on the data. In the outer LOOP
I went a bit crazy and used a lot of its abilities — initializing temporary variables, LOOP
conditionals, a FINALLY
clause — with the results that look like an Algol-Lisp chimera.If I were a code purist
weighted-average
would probably make me crazy. Good thing I'm not.
No comments:
Post a Comment