如果可能,我会使用像 map 这样的高阶函数,它会在底层构建一个列表。在 Common Lisp 中,我也经常使用 loop,它有一个 collect 关键字,用于以向前的方式构建列表(我还使用 series 库,它也透明地实现了它)。
我有时会使用非尾递归的递归函数,因为它们能更好地表达我想要的,而且列表的大小会相对较小;特别是在编写宏时,被操作的代码通常不会很大。
对于我不收集到列表中的更复杂的问题,我通常接受为每个解决方案调用的回调函数。这样可以确保更清楚地区分数据的产生方式和使用方式。
这种方法对我来说是最灵活的,因为没有假设应该如何处理或收集数据。但这也意味着回调函数可能会执行副作用或非本地返回(参见下面的示例)。只要副作用的范围很小(函数局部),我认为这不是一个特别的问题。
例如,如果我想要一个函数生成 0 到 N-1 之间的所有自然数,我会写:
(defun range (n f)
(dotimes (i n)
(funcall f i)))
这里的实现会遍历从 0 到 N 的所有值,并用值 I 调用 F。
如果我想将它们收集到一个列表中,我会写:
(defun range-list (N)
(let ((list nil))
(range N (lambda (v) (push v list)))
(nreverse list)))
但是,我也可以通过使用队列来避免整个 push/nreverse 成语。 Lisp 中的队列可以实现为一对 (first . last),它跟踪底层链表集合的第一个和最后一个 cons 单元。这允许以恒定的时间将元素附加到末尾,因为不需要遍历列表(参见 P. Norvig 的 在 Lisp 中实现队列,1991)。
(defun queue ()
(let ((list (list nil)))
(cons list list)))
(defun qpush (queue element)
(setf (cdr queue)
(setf (cddr queue)
(list element))))
(defun qlist (queue)
(cdar queue))
因此,该函数的替代版本是:
(defun range-list (n)
(let ((q (queue)))
(range N (lambda (v) (qpush q v)))
(qlist q)))
当您不想构建所有元素时,生成器/回调方法也很有用;这有点像惰性评估模型(例如 Haskell),您只使用您需要的项目。
假设您想使用range 查找vector 中的第一个空槽,您可以这样做:
(defun empty-index (vector)
(block nil
(range (length vector)
(lambda (d)
(when (null (aref vector d))
(return d))))))
这里,词法名称nil的block允许匿名函数调用return以返回值退出块。
在其他语言中,相同的行为通常由内而外颠倒过来:我们使用带有游标和下一个操作的迭代器对象。我倾向于认为简单地编写迭代并调用回调函数会更简单,但这也是另一种有趣的方法。