【问题标题】:Non-`loop`, non-mutating way to accomplish a loop in Lisp?在 Lisp 中完成循环的非循环、非变异方式?
【发布时间】:2019-07-12 13:51:30
【问题描述】:

我使用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 和一个累加器。有没有更简单的方法来实现与循环相同的效果,而无需借助突变且不会溢出堆栈?

【问题讨论】:

    标签: loops lisp common-lisp


    【解决方案1】:

    您也可以使用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】:

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

      确实没有保证您的第二个版本不会溢出 CL 中的堆栈:大多数当前实现会将尾调用编译为迭代,尽管可能无法在解释代码中处理此问题,但有些受其目标(例如 JVM)的限制不要这样做。也有一些主要的历史原生代码实现(例如 Symbolics CL)。

      有一些 Lisp 系列语言确实在语言中指定尾调用是迭代,特别是 Scheme,并且在此类语言中,您的第二个版本会很好。

      关于需要向后构建列表然后反转它们的问题:我认为这是列表在 Lisps 中工作方式的必然结果:你真的只能通过在列表的开头添加东西来构建列表,如果你不乐意更改现有列表或对每个步骤进行批量复制。

      当然,您可以在幕后隐藏您正在构建的列表的突变,这样您就永远不需要知道发生了什么,但这并不意味着它不会改变结构或向后构建然后反转.因此,例如,我有一个如下结构:

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

      它向前构建列表,但它通过保留尾指针并改变它正在构建的列表来做到这一点。

      【讨论】:

      • 由于 JVM 的固有限制,目前没有尾调用消除的一种实现是 ABCL。
      • 大多数 CL '解释器',如果不是全部,将不支持 TCO。
      • 我已经对 TCO 部分进行了重新措辞,希望能更多地代表现实。
      • ... 隐藏起来的突变并没有错。 :)
      • @WillNess:不,当然不是。它没有任何问题 (IMO),即使它不是,只要清楚发生了什么!
      【解决方案3】:

      您可以通过在内部调用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好。

      不是标准的迭代构造,但与LOOP 一样强大并且在美学上稍好一点的是ITERATE

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-09-23
        • 2010-09-10
        • 2011-04-28
        • 2011-09-18
        • 1970-01-01
        相关资源
        最近更新 更多