Lisp 递归实现任意序列的长度,而不仅仅是列表

Lisp 递归实现任意序列的长度,而不仅仅是列表,lisp,common-lisp,Lisp,Common Lisp,我是Lisp新手,希望用递归实现函数length。我编写以下代码只是为了发现它只能用于列表,而不能用于字符串 (defun mylen (l) (if (eq (cdr l) nil) 1 (+ 1 (mylen (cdr l))))) 我想知道我是否可以只编写一个既可以用于列表又可以用于字符串的函数 对任意序列使用序列函数 首先,请注意,Common Lisp适用于任何序列,而不仅仅是列表(也不仅仅是字符串,&c)。所以你根本不需要写这个。通常,还有一些其他函数在序列上运

我是Lisp新手,希望用递归实现函数
length
。我编写以下代码只是为了发现它只能用于列表,而不能用于字符串

(defun mylen (l)
  (if (eq (cdr l) nil)
    1
    (+ 1 (mylen (cdr l)))))
我想知道我是否可以只编写一个既可以用于列表又可以用于字符串的函数

对任意序列使用序列函数 首先,请注意,Common Lisp适用于任何序列,而不仅仅是列表(也不仅仅是字符串,&c)。所以你根本不需要写这个。通常,还有一些其他函数在序列上运行,例如。一般来说,编写在列表和向量上都有效的通用代码并不容易。实现通常通过先检查提供了什么样的序列,然后使用专门的版本来实现获取任意序列的函数。当这些功能可用时,使用它们对您有利。您可以在HyperSpec中查看更多操作任意序列的函数

您可以使用
映射
并实现一个
序列长度
,如下所示:

(defun sequence-length (sequence &aux (length 0))
  (map nil (lambda (x)
             (declare (ignore x))
             (incf length))
       sequence)
  length)
上面的
序列长度
只突出显示了
映射
对每个序列都有效。一个更为惯用的解决方案是使用另一个通常适用于序列的函数,
reduce
,因为(此代码基于其答案中的代码,但用于创建忽略其参数并返回
1
)的函数:

另一种选择是使用
count if
,如中所示

(count-if (constantly t) sequence)
它统计
(常为t)
返回true的项目数,即所有项目

编写自己的序列函数 不过,您正在实现的递归类型确实依赖于由
cons
单元格构建的列表,您可以轻松地处理列表的第一部分,然后处理列表的其余部分。一般来说,对于序列没有这样做的函数,因为获取向量的其余部分通常意味着获取向量的副本。通常,如果需要类似的内容,函数将获取开始和结束索引,并在这些索引上递归,而不是在序列上递归。例如:

(defun list-elements (sequence start end)
  (if (= start end)
      '()
      (cons (elt sequence start)
            (list-elements sequence (1+ start) end))))
reduce
的大多数实现只是检查序列是列表还是向量,然后分派到以最适当的方式处理这些序列的专用版本。例如,如果您查看(从第1242行开始),您将看到四种不同类型的宏的实现:

  • 咕哝减少
  • 自始至终喃喃自语
  • 列表减少
  • 列表从头到尾减少
  • 和一个序列遍历器:

    • 减少
    您需要更多地了解SBCL中的定义表单,以了解它们是如何使用的,但它们确实证明了实现泛型序列函数是一项工作

    为了完整性,第一个元素/序列的其余部分效率低下 在说了所有这些之后,我将指出,您可以使用对任意序列有效的
    subseq
    ,对向量和列表执行这种递归,但它实际上效率不高。例如,这里有一个简单的
    反转为list
    函数

    (defun reverse-into-list (sequence &optional (result '()))
      (if (eql 0 (length sequence))
          result
          (reverse-into-list (subseq sequence 1) 
                             (cons (elt sequence 0) result))))
    

    它使用
    length
    来确定何时停止,但您可以编写一个更高效的版本,首先检查它是否是一个列表,如果是,则检查它是否为空(常数时间),如果不是列表,则只调用length(也应该是常数时间).

    您的Lisp实现的标准库
    length
    函数可能通过访问某些内部属性来处理字符串和其他向量。但是,如果你想递归地计算向量的长度,最好的办法可能是
    将它们强制到列表中。向量并不像列表那样是固有的递归数据结构


    顺便说一句,您当前的实现对于列表也是有缺陷的。对于空列表,它将失败(即返回
    1
    ,而不是
    0
    )。通过简化它,您可以使它正确地用于空列表。

    可以从序列中提取“rest”,如果它是一个向量,则只会减少一小部分:


    只是为了补充约书亚的答案:不是
    map
    ,而是使用
    reduce

    (reduce #'+ sequence :key (constantly 1))
    

    在Lisp中,计算字符串的长度是毫无用处的,因为每个向量都将长度存储为一个属性—没有像C中那样的结束标记。因此,向量的长度—字符串是一个向量—不需要在Lisp中计算。

    如果您要将
    长度
    重新实现为
    mylen
    ,你可能不想使用
    length
    作为它的一部分。+1是我第一次错过的一个很好的直接答案!我在我的答案中添加了一个版本,但我使用了
    (经常是1)
    作为关键。@Joshua Taylor:非常感谢您的改进!我现在也把代码改成了那样。
    CL-USER> (list-elements "abcdefghijklmnop" 3 7)
    (#\d #\e #\f #\g)
    
    (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)
    
    (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)))))
    
    (reduce #'+ sequence :key (constantly 1))