I was reading the book On Lisp by Paul Graham. In Chapter 4, Utility Functions, he gives examples of small functions that operate on lists, which would be helpful while writing a larger program.
One of them is flatten
. Given a nested list at any arbitrary level as argument, flatten will remove all the nested elements and put them on the top level.
Below is my attempt at implementing flatten:
(defun flatten (lst)
(labels ((rflatten (lst1 acc)
(dolist (el lst1)
(if (listp el)
(rflatten el acc)
(push el acc)))
acc))
(reverse (rflatten lst nil))))
But the above function does not flatten lists properly.
; returns (1) instead of (1 2)
(print (flatten '(1 (2))))
(1)
Calling the function with (1 (2))
returns (1)
instead of (1 2)
.
I cannot find what's wrong with my implementation of flatten. Is it the way I am using
labels
? Or is it the the way I am using the dolist
macro? The dolist
macro always returns nil
. But that should not matter as I am using an accumulator acc
to store the flattened list.
push
changes the symbol binding in scope. Thus the recursion(rflatten el acc)
has it's ownacc
which is the result there but you don't do anything with the returned result and it doesn't alter the calleeacc
.Perhaps a
(setf acc (rflatten el acc))
would fix that:A non-recursive code which builds the result by
cons
es, following comments and starting from a code by user:Sylwester:It's not pretty, but it seems to work. Parallel assignment
PSETQ
is used to simulate tail-recursive call frame update without worrying about precise sequencing.Implements the same process as the one encoded nicely by
with implicit stack operations explicated as list structure manipulations among the variables.
You're actually very close. As Sylwester mentions, the issue is that (push el acc) only modifies the local binding of el (of which there's a new one for each call to rflatten. As Rainer mentions, it's not really an accumulator in the traditional sense, so I'm going not going to call it acc, but result. Since you're already defining a local function, there's no reason not to define result in a wider scope:
There are actually a few ways to clean this up, too. The first is a matter of style and taste, but I'd use an &aux variable to bind result, so
The next is that dolist can take a third argument, a form to evaluate as for the return value. This is often used in a "push to create a list, then reverse for the return value" idiom, e.g.,
You don't want to reverse after every dolist, but you can still return result from the dolist, and thus from rflatten. Then you can simply call nreverse with the result of rflatten: