在Lisp中完成循环的非循环非变异方式?

时间:2019-02-19 06:09:07

标签: loops lisp common-lisp

我使用local-time编写了以下循环:

(defun count-dates (stop-date k)
  (loop for step = (local-time:today)
        then (local-time:timestamp- step 1 :day)
        while (local-time:timestamp>= step stop-date)
        collect (funcall k step)))

它可以像这样简单地运行:

(count-dates (local-time:encode-timestamp 0 0 0 0 1 1 2019) #'princ)

虽然这很简单明了,但我想知道如何在没有强大的loop构造的情况下编写它,并提出:

(defun count-dates2 (stop-date k)
  (reverse (labels ((f (acc step)
                      (if (local-time:timestamp>= step stop-date)
                          (f (cons (funcall k step) acc)
                             (local-time:timestamp- step 1 :day))
                          acc)))
             (f '() (local-time:today)))))

使用reverse和一个累加器,这似乎太复杂了。是否有一种更简单的方法来实现与循环相同的效果,而不必求助于突变并且不会导致堆栈溢出?

3 个答案:

答案 0 :(得分:7)

不是在Common Lisp中,不是:如果需要迭代构造,则需要使用显式迭代构造:CL不能保证语法递归构造实际上是迭代的。 loop并不是唯一的迭代结构,您当然可以编写自己的迭代和结果收集结构。

实际上,我们无法保证您的第二个版本不会在CL中溢出堆栈:大多数当前的实现将编译尾部调用,因为迭代虽然可能无法在解释的代码中处理,但是有些受其目标(例如JVM)的约束。不要这样做。也有一些主要的历史性本机代码实现(例如Symbolics CL)没有。

有Lisp族的语言确实指定了尾调用是迭代的语言,尤其是Scheme,在这种语言中,您的第二个版本就可以了。

关于需要向后建立列表然后将它们反向的问题:我认为这是Lisps中列表工作方式的必然结果:如果您真正要建立列表,则只能在列表的开头添加内容不愿意更改现有列表或不愿意为每个步骤批量复制。

当然,您可以在后台隐藏正在构建的列表的变异,这样您就无需知道发生了什么,但这并不意味着它既不会变异结构,也不会反向构建然后反转。因此,例如,我有一个看起来像这样的构造:

(collecting
  ...
  (collect ...)
  ...)

会向前构建列表,但是它是通过保留尾指针并对其正在构建的列表进行变异来实现的。

答案 1 :(得分:6)

您还可以使用SERIES软件包:

(defpackage :so (:use :cl :series :local-time))
(in-package :so)

(let ((stop-date (timestamp- (today) 10 :day)))
  (scan-fn  ;; type of elements (could be T here)
            'timestamp
            ;; init function
            (lambda () (today))
            ;; step function
            (lambda (ts) (timestamp- ts 1 :day))
            ;; termination test
            (lambda (ts) (not (timestamp>= ts stop-date)))))

上面的代码返回一个序列对象的实例,该对象是一个有效地编译的延迟(按需)值流。在REPL中,它显示为#Z(...)(其中点是元素)。如果要将其转换为列表,可以致电collect

(collect *) ;; assuming * is the last returned value

如果您想使用向量,则:

(collect 'vector **)

哪个给:

#(@2019-02-19T01:00:00.000000+01:00 @2019-02-18T01:00:00.000000+01:00
  @2019-02-17T01:00:00.000000+01:00 @2019-02-16T01:00:00.000000+01:00
  @2019-02-15T01:00:00.000000+01:00 @2019-02-14T01:00:00.000000+01:00
  @2019-02-13T01:00:00.000000+01:00 @2019-02-12T01:00:00.000000+01:00
  @2019-02-11T01:00:00.000000+01:00 @2019-02-10T01:00:00.000000+01:00
  @2019-02-09T01:00:00.000000+01:00)

还要注意,在collect用词法包围scan-fn函数的情况下,它可以直接将代码表示为循环。例如:

(let ((stop-date (timestamp- (today) 10 :day)))
  (collect
      (scan-fn  ;; type of elements (could be T here)
       'timestamp
       ;; init function
       (lambda () (today))
       ;; step function
       (lambda (ts) (timestamp- ts 1 :day))
       ;; termination test
       (lambda (ts) (not (timestamp>= ts stop-date))))))

collect格式被宏扩展为:

(LET* (#:STATE-1062 #:ITEMS-1063 (#:LASTCONS-1060 (LIST NIL)) #:LST-1061)
  (DECLARE (TYPE CONS #:LASTCONS-1060)
           (TYPE LIST #:LST-1061))
  (LOCALLY
   (DECLARE (TYPE TIMESTAMP #:STATE-1062)
            (TYPE TIMESTAMP #:ITEMS-1063))
   (SETQ #:STATE-1062 ((LAMBDA () (TODAY))))
   (SETQ #:LST-1061 #:LASTCONS-1060)
   (TAGBODY
    #:LL-1064
     (IF ((LAMBDA (TS) (NOT (TIMESTAMP>= TS STOP-DATE))) #:STATE-1062)
         (GO SERIES::END))
     (SETQ #:ITEMS-1063 #:STATE-1062)
     (SETQ #:STATE-1062 ((LAMBDA (TS) (TIMESTAMP- TS 1 :DAY)) #:STATE-1062))
     (SETQ #:LASTCONS-1060
             (SETF (CDR #:LASTCONS-1060) (CONS #:ITEMS-1063 NIL)))
     (GO #:LL-1064)
    SERIES::END)
   (CDR #:LST-1061)))

如Evhince所述,Common Lisp食谱有关于系列的部分,请参见https://lispcookbook.github.io/cl-cookbook/iteration.html

答案 2 :(得分:5)

通过将reverse调用引入内部,可以摆脱一种缩进。 还要注意,count-dates这个名字并不好,因为它不是在计算日期,而是映射从今天到stop-date的一天之内的函数。

(defun count-dates2 (stop-date k)
  (labels ((f (acc step)
             (if (local-time:timestamp>= step stop-date)
                 (f (cons (funcall k step) acc)
                    (local-time:timestamp- step 1 :day))
                 (reverse acc))))
    (f '() (local-time:today)))))

另一个迭代构造是旧的DO

(defun count-dates (stop-date k &aux result)
  (do ((step
        (local-time:today)
        (local-time:timestamp- step 1 :day)))

      ((not (local-time:timestamp>= step stop-date))
       (reverse result))

    (push (funcall k step) result)))

但这并不比LOOP好。

ITERATE是一种非标准的迭代构造,但功能与LOOP一样强大,并且在美学上稍好一些。