学习口齿不清的最困难的部分可能是以“优等生活方式”思考,这种方式既优雅又令人印象深刻,但并不总是那么容易。我知道递归用于解决许多问题,我正在编写一本书,而是使用apply
来解决许多问题,我理解这些问题并不像lispy那样,也不像便携式。< / p>
经验丰富的lisper应该能够帮助解决这个逻辑,而不必具体了解describe-path
location
和edges
所指的内容。以下是我正在编写的一本书中的一个例子:
(defun describe-paths (location edges)
(apply (function append) (mapcar #'describe-path
(cdr (assoc location edges)))))
我已成功重写此内容以避免apply
并改为使用递归。它似乎有效:
(defun describe-paths-recursive (location edges)
(labels ((processx-edge (edge)
(if (null edge)
nil
(append (describe-path (first edge))
(processx-edge (rest edge))))))
(processx-edge (cdr (assoc location edges)))))
我想要一些经验丰富的眼睛来建议是否有更优雅的方式将apply
转换为递归,或者我是否做了一些不明智的事情。这段代码看起来不错,但是会不会有更多“lispy”?
答案 0 :(得分:6)
(apply (function append) (mapcar #'g ...))
只是mapcan
(更新: with usual caveats有关破坏性更新和引用列表,另请参阅this):
(defun describe-paths (location edges)
(mapcan #'describe-path
(cdr (assoc location edges))))
递归有助于思考,理解。但实际上在代码中使用它需要付出代价。
你的递归重写是tail recursive modulo cons;没有Lisp有这个优化AFAIK,即使是it was first described in 1974,在Lisp 。
所以你写的是可执行规范。
但Common Lisp是一种实用语言。特别是,它有许多方法来编码迭代。请记住,迭代过程是我们的目标;递归过程是可怕的,效率明智的。因此,当我们编写一个语法递归的代码时,我们仍然希望它描述一个迭代过程(这样在不断的堆栈空间中运行)。
Common Lisp,作为一种实用语言,我们只需要直接编写循环。一个人,
(defun describe-paths-loop (location edges &aux (res (list 1)) (p res))
(dolist (x (cdr (assoc location edges))
(cdr res)) ; the return form
(setf (cdr p) (describe-path x))
(setf p (last p))))
保证在常量堆栈空间中工作。
更新:这会破坏性地连接 describe-path
返回的列表,因此请注意不要返回具有相同last
的列表在单独的调用上利用单元格,或者这可以创建循环结构。或者,对describe-path
的调用可以包含在copy-list
电话中。当然,如果describe-path
要返回一个已经循环的列表,last
这里也会进入循环。
答案 1 :(得分:3)
我看到几个关于使用apply
的意见是一种糟糕的风格。但实际上,如果有人会解释为什么apply
被认为是坏的,那将是很好的。
你是什么意思用“lispy”这个词。常见的lisp允许以您想要的任何样式进行编程。
如果“lispy”表示函数式编程风格,那么第一个代码将以更多函数编程风格编写。函数传递给函数mapcar
,另一个函数传递给apply
,所有工作都是通过将一个函数的结果传递给另一个函数来完成的。在您的代码中,您不会将函数作为参数传递给其他函数。但是递归可以被认为是函数式编程风格的标志。书中的代码比你的短。
如果因为apply而不喜欢apply
确定运行时的参数计数,则可以在这种情况下使用reduce
(如果我正确理解了数据结构):
(感谢Joshua Taylor在没有:from-end t
关键参数的情况下指出了巨大的资源开销)
(defun describe-paths (location edges)
(reduce #'append (mapcar #'describe-path
(rest (assoc location edges))) :from-end t))
无论如何,我很确定书中代码的目的是教育原因。这是mapcar
和apply
的一个示例,它显示了如何将列表视为lisp中的数据和代码。
P.S。实际上我想到了为什么apply
可能是坏的(堆栈用于函数调用)。
> (apply #'+ (make-list 500000 :initial-element 1))
*** - Lisp stack overflow. RESET
因为Rainer Joswig告诉它避免堆栈溢出是好事。减少修复问题。
> (reduce #'+ (make-list 50000000 :initial-element 1))
50000000
答案 2 :(得分:3)
Lisp方法是使用函数式,命令式或面向对象的编程(有或没有可变状态)来解决问题,或者根据您的需要发明一些其他编程并在宏中表达它。寻找递归而忽略其他方法不是Lisp方式;这是任性的Lisp学术的方式。
重写函数最直接的方法:
(defun describe-paths (location edges)
(apply (function append) (mapcar #'describe-path
(cdr (assoc location edges)))))
是使用loop
。消除适用的正确动机是我们期望许多路径,这可能超过函数参数数量的限制。
您正在使用apply
做的所有事情都是append
函数的一个大参数列表。我们可以将任意数量的列表附加到loop
这样的大列表中:
(defun describe-paths (location edges)
(loop for path in (cdr (assoc location edges))
appending (describe-path path))
据推测,describe-path
会返回一个列表,并且您希望将这些列表组合在一起。
appending
的{{1}}子句,也可以拼写为loop
,收集将参数形式的值附加到匿名列表中。当循环终止时,该列表成为返回值。
如果我们有理由相信append
返回的列表是在每次调用时新分配的,我们可以使用nconcing
来提高性能。
答案 3 :(得分:-1)
这个问题没有错;例如,在python类别中提出了许多与此类似的问题。
但是对于你的问题:你在做什么是好的。事实上,它几乎完全类似于Peter Norvig在他的一本Lisp书中所展示的更为一般的技巧,所以你要么读过那本书,要么你自己偶然发现了一个好的练习。无论哪种方式,这都是一个完全可以接受的递归实现。