【发布时间】:2012-10-26 16:23:56
【问题描述】:
我见过几个将append 元素实现到列表的示例,但都没有使用尾递归。如何在函数式风格中实现这样的功能?
(define (append-list lst elem)
expr)
【问题讨论】:
标签: lisp scheme racket tail-call-optimization tailrecursion-modulo-cons
我见过几个将append 元素实现到列表的示例,但都没有使用尾递归。如何在函数式风格中实现这样的功能?
(define (append-list lst elem)
expr)
【问题讨论】:
标签: lisp scheme racket tail-call-optimization tailrecursion-modulo-cons
嗯,可以编写一个尾递归append-element 过程...
(define (append-element lst ele)
(let loop ((lst (reverse lst))
(acc (list ele)))
(if (null? lst)
acc
(loop (cdr lst) (cons (car lst) acc)))))
...但是使用reverse 会更加低效(为了更好的衡量标准)。我想不出另一种功能(例如,不修改输入列表)方法来将此过程编写为尾递归,而无需先反转列表。
对于问题的非功能性答案,@WillNess 提供了一个很好的 Scheme 解决方案来改变内部列表。
【讨论】:
set!、set-car!、set-cdr! 这不再被视为功能解决方案时,您正在更改状态 - 在这种情况下是 cons 单元格。即使它在外面看起来很实用。
snoc 操作或使用额外的reverse,则无法做到这一点。
这是 Lisp,不是 Scheme,但我相信你可以翻译:
(defun append-tail-recursive (list tail)
(labels ((atr (rest ret last)
(if rest
(atr (cdr rest) ret
(setf (cdr last) (list (car rest))))
(progn
(setf (cdr last) tail)
ret))))
(if list
(let ((new (list (car list))))
(atr (cdr list) new new))
tail)))
我保留返回列表的头部和尾部,并在遍历列表参数时修改尾部。
【讨论】:
以下是tail recursion modulo cons优化的实现,产生了完全尾递归的代码。它复制输入结构,然后以自上而下的方式通过突变将新元素附加到它。由于这种突变是对其内部新创建的数据进行的,因此它在外部仍然有效(不会改变传递给它的任何数据,并且除了产生结果之外没有可观察到的影响):
(define (add-elt lst elt)
(let ((result (list 1)))
(let loop ((p result) (lst lst))
(cond
((null? lst)
(set-cdr! p (list elt))
(cdr result))
(else
(set-cdr! p (list (car lst)))
(loop (cdr p) (cdr lst)))))))
我喜欢使用“head-sentinel”技巧,它大大简化了代码,但代价是只分配了一个额外的 cons 单元格。
此代码使用低级突变原语来完成某些语言(例如 Prolog)中由编译器自动完成的工作。在 TRMC 优化假设方案中,我们将能够编写以下尾递归 模 cons 代码,并让编译器自动将其转换为与上述代码等效的代码:
(define (append-elt lst elt) ;; %% in Prolog:
(if (null lst) ;; app1( [], E,R) :- Z=[X].
(list elt) ;; app1( [A|D],E,R) :-
(cons (car lst) ;; R = [A|T], % cons _before_
(append-elt (cdr lst) elt)))) ;; app1( D,E,T). % tail call
如果不是 cons 操作,append-elt 将是尾递归的。这就是 TRMC 优化发挥作用的地方。
2021 年更新: 当然,拥有 tail-recursive 函数的全部意义在于表达一个循环(以函数式风格,是的),因此作为一个例如,在例如Common Lisp(在CLISP实现中),循环表达式
(loop for x in '(1 2) appending (list x))
(这是一种高级规范——即使不是以它自己非常具体的方式发挥作用)被翻译成相同的尾部缺点细胞跟踪和改变风格:
[20]> (macroexpand '(loop for x in '(1 2) appending (list x)))
(MACROLET ((LOOP-FINISH NIL (SYSTEM::LOOP-FINISH-ERROR)))
(BLOCK NIL
(LET ((#:G3047 '(1 2)))
(PROGN
(LET ((X NIL))
(LET ((#:ACCULIST-VAR-30483049 NIL) (#:ACCULIST-VAR-3048 NIL))
(MACROLET ((LOOP-FINISH NIL '(GO SYSTEM::END-LOOP)))
(TAGBODY SYSTEM::BEGIN-LOOP (WHEN (ENDP #:G3047) (LOOP-FINISH))
(SETQ X (CAR #:G3047))
(PROGN
(LET ((#:G3050 (COPY-LIST (LIST X))))
(IF #:ACCULIST-VAR-3048
(SETF #:ACCULIST-VAR-30483049
(LAST (RPLACD #:ACCULIST-VAR-30483049 #:G3050)))
(SETF #:ACCULIST-VAR-30483049
(LAST (SETF #:ACCULIST-VAR-3048 #:G3050))))))
(PSETQ #:G3047 (CDR #:G3047)) (GO SYSTEM::BEGIN-LOOP) SYSTEM::END-LOOP
(MACROLET
((LOOP-FINISH NIL (SYSTEM::LOOP-FINISH-WARN) '(GO SYSTEM::END-LOOP)))
(RETURN-FROM NIL #:ACCULIST-VAR-3048)))))))))) ;
T
[21]>
(所有结构变异原语的母体拼写为R.P.L.A.C.D.)所以这是一个实际上做类似事情的 Lisp 系统(不仅仅是 Prolog)的一个例子。
【讨论】:
您不能天真,但另请参阅提供 TCMC - Tail Call Modulo Cons 的实现。这允许
(cons head TAIL-EXPR)
如果 cons 本身是尾调用,则尾调用 TAIL-EXPR。
【讨论】:
这是一个使用延续的函数式尾递归追加elt:
(define (cont-append-elt lst elt)
(let cont-loop ((lst lst)
(cont values))
(if (null? lst)
(cont (cons elt '()))
(cont-loop (cdr lst)
(lambda (x) (cont (cons (car lst) x)))))))
在性能方面,它接近威尔在 Racket 和 Gambit 中的变异之一,但在 Ikarus 和 Chicken Óscar 的反面中表现更好。突变总是表现最好的。不过我不会使用这个,而是 Óscar 条目的一个小版本,纯粹是因为它更容易阅读。
(define (reverse-append-elt lst elt)
(reverse (cons elt (reverse lst))))
如果你想要改变性能,我会这样做:
(define (reverse!-append-elt lst elt)
(let ((lst (cons elt (reverse lst))))
(reverse! lst)
lst))
【讨论】:
foldr (:) [a] xs,或(set-cdr! (last-pair (copy-list xs)) (list a)),或== (reverse (cons elt (reverse lst))),或英文。 -- 自顶向下 O(1) 额外空间 TRMC 代码的要点是,我们可以做得比将额外 O(n) 增长堆栈结构换成额外 O(n) 增长延续结构更好,((((id.(1:)).(2:)).(3:)).(4:)) [5]。实际上,为了构建结果,它执行了两遍 O(n),而它的 Haskell 等价物执行三遍,自上而下的 TRMC 代码 - 一遍。