In defense of LOOP
Joe Marshall gives some reasons to hate LOOP and I can't say I disagree with him, save for has last point.
LOOP encourages users to think linearly, sequentially, and iteratively. They start trying to flatten everything into a single pass, first-to-last, state-bashing mess. Suddenly they forget about recursion. I've seen this happen over and over again. It literally damages the thought process.
This alludes to the old iteration versus recursion quagmire which is analogous to static versus dynamic "debates." Joe's self-professed knee-jerk reaction isn't going to get me to stop me from using LOOP, nor will it convince me to use recursion more. With both, I will use the one that fits the problem best given the context in which it is written.
Mapping over hash tables is one such example. Suppose I have a hash table whose values are lists and I want to calculate the sum of the length of all the lists. LOOP provides a succinct and clear way to express it.
(loop for list being each hash-value in htable sum (length list))
The MAPHASH and WITH-HASH-TABLE-ITERATOR approaches aren't quite as simple (although, admittedly, not difficult for this problem).
LOOP is also really handy for short-circuiting.
(defun first-n-things (list pred n) (loop with count = 0 for e in list when (funcall pred e) collect e and do (incf count) until (= count n)))
I can't say I'm fond of the LABELS approach to the latter.
(defun first-n-things-2 (list pred n) (labels ((helper (elems count acc) (cond ((or (null elems) (= n count)) acc) ((funcall pred (first elems)) (helper (rest elems) (1+ count) (cons (first elems) acc))) (t (helper (rest elems) count acc))))) (helper list 0 nil)))
Using an &OPTIONAL parameter as an accumulator is only slightly cleaner.
(defun first-n-things-3 (list pred n &optional acc) (cond ((or (null list) (zerop n)) acc) ((funcall pred (first list)) (first-n-things-3 (rest list) pred (1- n) (cons (first list) acc))) (t (first-n-things-3 (rest list) pred n acc))))
Debunking the "LOOP damages the thought process" claim isn't the point here, since I don't think it was meant to be an assertion in the first place. LOOP makes dealing with state a lot cleaner when it comes up, but I agree that there is no need to use it when a simple MAPCAR will do. Sometimes recursion is messy, sometimes it isn't. Frankly, I go back forth between the two all the time, trying to find the best way to express my problem, just like I do when I'm writing. I don't always succeed, but I want to keep my options open.

9 comments:
This is just such a stupid debate. Lisp is not a fricking "functional language"! It's a system where you change the language to your needs and the environment changes with you as you interactively develop code to solve your program.
Paul Graham might have misunderstood some things about Lisp, but he clearly understood one thing, lisp don't force you to do to things in one way, but gives you the flexibility to choose.
On a more theoretical note, a smaller and more abstract program is easier to get right and elegant. Your examples with LOOP and the alternatives just don't need any comment really. I just had to rant.
I'd prefer the functional solution:
(defun first-n-things (list pred n)
(remove-if-not pred (subseq list 0 n)))
Generally I agree: Sometimes loop is just the best solution.
Does anonymous's `first-n-things' work correctly? It returns N items only if the first N elements of LIST all satisfy PRED.
@andreas: Ranting is fine, as long as it's acknowledged as such.
@anonymous: Your FIRST-N-THINGS isn't the same as mine.
(defparameter *list* (loop for i from 1 to 100 collect i))
(woz:first-n-things *list* #'oddp 3) => (1 3 5)
(anonymous:first-n-things *list* #'oddp 3) => (1 3)
I was looking to return a list of the first N things that satisfy some predicate; admittedly, the function has a crappy name A functional solution (using the same constructs as you proposed) woud be
(subseq (remove-if-not pred list) 0 n)
Which is fine if you don't care about the extra consing that occurs. That's a topic for another day. :)
Note that (subseq (remove-if-not pred list) 0 n) should actually be rewritten as (subseq (remove-if-not predicate sequence) 0 n)). This would make it clearer that it is more general: it works on any sequence, not just lists. The output is also of the same sequence type.
The same cannot be said for the loop version. I think that illustrates some of the points raised by Joe Marshall.
To deal with consing, you could use another approach; it's hardly the case that the only alternative is loop.
This would make it clearer that it is more general: it works on any sequence, not just lists. The output is also of the same sequence type.
Indeed, if I were writing the function in a context where I expected to have more than just lists, then I would alter the names to show that it works with sequences. In this case I wasn't -- I was just illustrating the expressive differences.
Oh, my code is absolutely wrong. Shame on me.
My complaint about LOOP is there's no standard way to extend it.
The recursion for the sake of language purity argument leaves me completely cold, though. You want purity, go put on a hairshirt and program in Scheme.
hi, i'm a beginner and explorer of LISP. Can you tell me how to use LISP? it's hard to understand. X_-
We have a project to understand this program. But i don't know where should i begin. O.o
Post a Comment