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 < A < B > B > A < B > B) 中的 OP 示例输入的 10 次演变,在结果到达之前存在显着延迟。由于 OP 声明“我可能需要改进数十次”,因此这种延迟是一个真正的问题。
可以通过将append 移出迭代过程来改善这种情况。由于cons 便宜得多,我们可以使用cons 将子列表添加到累加器的前面,而不是将它们附加到末尾。当达到基本情况时,必须在append将所有子列表组合在一起以获得最终结果之前反转累加器。在非正式测试中,与原始定义相比,这种改进的过程导致了大约 10 倍的加速; (evolve-n 10 lsys A A < A < B > B > A < B > 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 < A < B > B > A < B > B) 进行的几项非正式测试表明,两个版本(使用辅助函数的版本和仅使用宏定义的版本)具有无法区分的性能;完成任务都需要 20 到 24 秒。在没有任何显着的性能优势的情况下,我会坚持使用更简单的辅助功能版本evolve-n。或者我会重新考虑输入和输出应该是什么样子;为什么不直接获取符号列表作为输入?