【问题标题】:Idiomatically avoiding recursion limit in scheme惯用地避免方案中的递归限制
【发布时间】:2020-05-02 08:57:38
【问题描述】:

请注意,我正在使用为我的学校定制的方案实现,因此功能可能看起来不熟悉,您的解决方案可能无法直接运行。我正在寻找更多的通用方法。

我有一个递归宏,可以演化出一个 L 系统。基本上,它看起来像这样:

(evolve lsys A A < A < B > B > A < B > B)
;; (lsys A A A A < A A < A < B > B > A < B > B > A A < A < B > B > A < B > B)

所以我构造了一个宏来做这个:

(evolve-n 3 lsys ...)
;; (evolve evolve evolve lsys ....)

现在的问题是,这个列表很快就会变得非常大。经过五次进化,我已经超过了递归限制。我可以只增加堆栈大小,但实际上不可行,因为我可能需要发展数十次。

evolve 的工作方式是采用可变数量的参数,第一个参数进行有条件的比较,然后附加到对列表其余部分调用 Evolution 的结果。像这样的:

(define-macro (evolve (variadic ls))
    (if (eq? ls '())
        '()
        (let ((l (car ls)))
            (append 
                (cond ((eq? l 'A) '(A A))
                      ((eq? l 'B) '(A < B > B))
                      (else (list l)))
                (apply evolve (cdr ls))))))

那么我怎样才能解开这种类型的东西,让它以迭代而不是递归的方式工作呢?

【问题讨论】:

  • 为什么是宏?
  • 因为我们处理的是抽象语法:)
  • 我的意思是“为什么它完全是一个宏”?您应该能够在“顶级”拥有一个宏,以避免评估参数并将它们传递给执行工作的常规过程。

标签: recursion scheme


【解决方案1】:

evolve


听起来您可能正在使用CS 61A Scheme 实现。

原则上,您应该做的是将工作从宏移到辅助过程中,然后从宏中调用

辅助过程应该使用累加器来存储结果,以便递归调用处于尾部位置,提供迭代过程;每次调用辅助过程时,一个新的符号列表appended 到累加器的末尾,但是当过程返回时没有更多工作要做。

(define (evolve-helper xs acc)
  (if (null? xs)
      acc
      (let ((x (car xs)))
        (evolve-helper (cdr xs)
                       (append acc
                               (cond ((eq? x 'A) '(A A))
                                     ((eq? x 'B) '(A < B > B))
                                     (else (list x))))))))

调用此过程的宏是可变参数的。由于宏用于创建要评估的表单,因此该宏必须创建的不是列表,而是一个评估为列表的表单。 Quasiquotation 用于创建计算结果为列表的最终形式。

(define-macro (evolve . xs)
  `(quote ,(evolve-helper xs '())))

根据文档,所有这些都应该在 CS 61A 实现中起作用。但是,如果您愿意:

(define-macro (evolve-2 (variadic xs))
  (quasiquote (quote (unquote (evolve-helper xs '())))))

REPL 交互示例:

scheme@(guile-user)> (evolve lsys A A < A < B > B > A < B > B)
$8 = (lsys A A A A < A A < A < B > B > A < B > B > A A < A < B > B > A < B > B)

scheme@(guile-user)> (evolve-2 lsys A A < A < B > B > A < B > B)
$9 = (lsys A A A A < A A < A < B > B > A < B > B > A A < A < B > B > A < B > B)

evolve-n


帮助程序也应该与evolve-n 一起使用。这个助手不需要调用evolve 宏,而是调用evolve-helper 过程。对evolve-n的递归调用处于尾部位置,所以这是一个迭代过程;每个调用只是将一个演化的参数列表传递给下一个。

(define (evolve-n-helper n xs)
  (if (zero? n)
      xs
      (evolve-n-helper (- n 1)
                       (evolve-helper xs '()))))

(define-macro (evolve-n n . xs)
  `(quote ,(evolve-n-helper n xs)))

您可以通过在evolve-n 宏中使用eval 来避免使用辅助过程,但是在过程也可以使用eval 时使用这种方式是不好的。

(define-macro (evolve-n-2 n . xs)
  (if (zero? n)
      `(quote ,xs)
      `(evolve-n-2 ,(- n 1) ,@(eval `(evolve ,@xs) (interaction-environment)))))

REPL 交互示例:

scheme@(guile-user)> (evolve-n 0 lsys A A < A < B > B > A < B > B)
$6 = (lsys A A < A < B > B > A < B > B)

scheme@(guile-user)> (evolve-n 1 lsys A A < A < B > B > A < B > B)
$7 = (lsys A A A A < A A < A < B > B > A < B > B > A A < A < B > B > A < B > B)

scheme@(guile-user)> (evolve-n 2 lsys A A < A < B > B > A < B > B)
$8 = (lsys A A A A A A A A < A A A A < A A < A < B > B > A < B > B > A A < A < B > B > A < B > B > A A A A < A A < A < B > B > A < B > B > A A < A < B > B > A < B > B)

scheme@(guile-user)> (evolve-n-2 2 lsys A A < A < B > B > A < B > B)
$9 = (lsys A A A A A A A A < A A A A < A A < A < B > B > A < B > B > A A < A < B > B > A < B > B > A A A A < A A < A < B > B > A < B > B > A A < A < B > B > A < B > B)

性能问题


append 过程很昂贵,并且根据上面evolve-helper 的定义,每次递归调用都会调用这个昂贵的过程。当输入变大时,这会增加。特别是,对于(evolve-n 10 lsys A A &lt; A &lt; B &gt; B &gt; A &lt; B &gt; B) 中的 OP 示例输入的 10 次演变,在结果到达之前存在显着延迟。由于 OP 声明“我可能需要改进数十次”,因此这种延迟是一个真正的问题。

可以通过将append 移出迭代过程来改善这种情况。由于cons 便宜得多,我们可以使用cons 将子列表添加到累加器的前面,而不是将它们附加到末尾。当达到基本情况时,必须在append将所有子列表组合在一起以获得最终结果之前反转累加器。在非正式测试中,与原始定义相比,这种改进的过程导致了大约 10 倍的加速; (evolve-n 10 lsys A A &lt; A &lt; B &gt; B &gt; A &lt; B &gt; B) 的结果不到一秒钟就到达了。这也说明了为什么最好将尽可能多的工作从宏移到函数中;宏比常规函数更难编写,也更脆弱。通过仅在需要的地方使用宏并在其他地方使用功能,简化了此处进行必要的优化。

(define (evolve-helper xs acc)
  (if (null? xs)
      (apply append (reverse acc))
      (let ((x (car xs)))
        (evolve-helper (cdr xs)
                       (cons (cond ((eq? x 'A) '(A A))
                                   ((eq? x 'B) '(A < B > B))
                                   (else (list x)))
                             acc)))))

仅限宏观的解决方案


evolve 宏可以通过映射一个对输入应用转换规则的过程来完全避免自定义外部帮助函数和显式递归。

(define-macro (evolve . xs)
  `(quote ,(apply append (map (lambda (x) (cond ((eq? x 'A) '(A A))
                                                ((eq? x 'B) '(A < B > B))
                                                (else (list x))))
                              xs))))

这似乎是一个不错的解决方案,但目前我没有看到在 evolve-n 宏中使用此定义的直接方法,除非使用 eval

(define-macro (evolve-n n . xs)
  (if (zero? n)
      `(quote ,xs)
      `(evolve-n ,(- n 1) ,@(eval `(evolve ,@xs) (interaction-environment)))))

使用(evolve-n 15 lsys A A &lt; A &lt; B &gt; B &gt; A &lt; B &gt; B) 进行的几项非正式测试表明,两个版本(使用辅助函数的版本和仅使用宏定义的版本)具有无法区分的性能;完成任务都需要 20 到 24 秒。在没有任何显着的性能优势的情况下,我会坚持使用更简单的辅助功能版本evolve-n。或者我会重新考虑输入和输出应该是什么样子;为什么不直接获取符号列表作为输入?

【讨论】:

  • 抱歉回复晚了!谢谢你。这太迷人了!
  • 了解执行时间与生成的列表大小的增长顺序可能会很有趣——它是线性的、二次的还是什么(我的个人资料中的 WP 链接),尤其是。随着从 append 到 cons 的变化(我希望将它从二次变为大约线性)。在两个尺寸点测量(运行时间不可忽略,比如 12 和 15)就足以得到一个粗略的estimate,比如 20 或 100。:)(作为参考/娱乐链接的问答)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-07-31
  • 2020-11-10
  • 2013-05-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多