Reputation: 3256
I am new to Lisp and want to implement the function length
with recursion. I have written the following code only to find that it can only work for lists but not for strings.
(defun mylen (l)
(if (eq (cdr l) nil)
1
(+ 1 (mylen (cdr l)))))
I wonder is it possible for me to write only one function that can both work for lists and strings?
Upvotes: 0
Views: 1996
Reputation: 85853
First, note that Common Lisp's length
works on any sequence, not just lists (and not just strings, &c.). So you don't actually need to write this at all. There are some other functions that operate over sequences in general, such as map
. In general, it's not all that easy to write generic code that if efficient on both lists and vectors. Implementations often implement functions that take arbitrary sequences by checking what kind of sequence has been provided first, and then using a specialized version. It's to your advantage to use those functions when they are available. You can have a look in the 17.3 Sequences Dictionary from the HyperSpec for more functions that operate on arbitrary sequences.
You could use map
and implement a sequence-length
like this:
(defun sequence-length (sequence &aux (length 0))
(map nil (lambda (x)
(declare (ignore x))
(incf length))
sequence)
length)
The sequence-length
above just highlights that map
works on each. A more idiomatic solution would use another function that works on sequences in general, reduce
, as Rainer Joswig points out (this code is based on the code in his answer, but uses constantly
to create a function that ignores its arguments and returns 1
):
(defun sequence-length (sequence)
(reduce '+ sequence :key (constantly 1)))
CL-USER> (sequence-length '(1 2 3))
3
CL-USER> (sequence-length "foobar")
6
Another alternative would be to use count-if
As in
(count-if (constantly t) sequence)
which counts the number of items for which (constantly t)
returns true, which is all of them.
The kind of recursion that you're implementing, though, does depend on lists built of cons
cells that you make it easy to handle the first part of a list and then the rest of the list. There aren't functions that do that for sequences in general, because getting the rest of a vector usually means taking a copy of it. Usually, if something like this is needed, the function takes a start and end index, and recurses on those, rather than on the sequence. E.g.,
(defun list-elements (sequence start end)
(if (= start end)
'()
(cons (elt sequence start)
(list-elements sequence (1+ start) end))))
CL-USER> (list-elements "abcdefghijklmnop" 3 7)
(#\d #\e #\f #\g)
Most implementations of reduce
are simply going to check whether the sequence is a list or a vector, and then dispatch to a specialized version that handles these in the most appropriate way. For instance, if you look at SBCL's implementation of reduce
(start around line 1242), you'll see implementations of four macros of some sort:
and one sequence traverser:
You'll need to know more about the defining forms in SBCL to know how those are all used, but they do demonstrate that implementing generic sequence functions is a bit of work.
After saying all that, I will point out that you can use subseq
, which works on arbitrary sequences, to do this kind of recursion on both vectors and lists, but it really won't be efficient. For instance, here's a trivial reverse-into-list
function.
(defun reverse-into-list (sequence &optional (result '()))
(if (eql 0 (length sequence))
result
(reverse-into-list (subseq sequence 1)
(cons (elt sequence 0) result))))
CL-USER> (reverse-into-list "hello")
(#\o #\l #\l #\e #\h)
CL-USER> (reverse-into-list '(1 2 3))
(3 2 1)
That used length
to determine when to stop, but you could write a more efficient version that first checks whether it's a list, and if it is, checks whether it's empty or not (constant time), and if it's not a list, just calls length (which should also be constant time).
Upvotes: 4
Reputation: 139261
Just to add to the answer of Joshua: not map
, use reduce
:
(reduce #'+ sequence :key (constantly 1))
To compute the length of strings is kind of useless in Lisp, since every vector stores the length as an attribute - there is no end marker like in C. Thus the length of a vector - and a string is a vector - does not need to be computed in Lisp.
Upvotes: 3
Reputation: 14305
It's possible to extract the "rest" from a sequence and only cons a small amount if it's a vector:
(defun sequence-rest (seq)
(if (listp seq)
(rest seq)
;; A full version would also care about element-type etc.
(make-array (1- (length seq)) :displaced-to seq
:displaced-index-offset 1)))
(defun mylen (seq)
(if (zerop (length seq))
1
(+ 1 (mylen (sequence-rest seq)))))
Upvotes: 1
Reputation: 6681
Your Lisp implementation's standard library length
function probably works on strings and other vectors by accessing some internal attribute. But if you want to calculate the length of vectors recursively anyway, your best bet would probably be to coerce
them to lists. Vectors just are not inherently recursive data structures the way that lists are.
By the way, your current implementation is buggy for lists, too. It will fail (i.e. return 1
instead of 0
) for empty lists. You can make it work correctly for empty lists by simplifying it.
Upvotes: 1