Lisp 将元素追加到列表的尾部递归函数

Lisp 将元素追加到列表的尾部递归函数,lisp,scheme,racket,tail-call-optimization,tailrecursion-modulo-cons,Lisp,Scheme,Racket,Tail Call Optimization,Tailrecursion Modulo Cons,我见过几个实现将元素附加到列表的示例,但都没有使用尾部递归。如何以功能性风格实现这样的功能 (define (append-list lst elem) expr) 可以编写一个尾部递归附加元素过程 (define (append-element lst ele) (let loop ((lst (reverse lst)) (acc (list ele))) (if (null? lst) acc (loop (

我见过几个实现
将元素附加到列表的示例,但都没有使用尾部递归。如何以功能性风格实现这样的功能

 (define (append-list lst elem)
    expr)
可以编写一个尾部递归
附加元素
过程

(define (append-element lst ele)
  (let loop ((lst (reverse lst))
             (acc (list ele)))
    (if (null? lst)
        acc
        (loop (cdr lst) (cons (car lst) acc)))))
。。。但是,如果将
反向
抛出(为了更好的衡量),则效率会更低。我想不出另一种函数式(例如,不修改输入列表)方法来将此过程编写为尾部递归,而不首先反转列表


对于这个问题的非功能性回答,@WillNess提供了一个很好的方案解决方案,它改变了一个内部列表。

这是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)))

我保留返回列表的头部和尾部,并在遍历列表参数时修改尾部。

下面是优化的一个实现,产生了一个完全尾部递归代码。它复制输入结构,然后通过变异以自上而下的方式将新元素附加到其中。由于这种变异是对其内部新创建的数据进行的,因此它在外部仍然起作用(不会改变传递给它的任何数据,除了产生结果外,没有任何可观察的影响):

我喜欢使用“headsentinel”技巧,它大大简化了代码,而只需分配一个额外的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更新:当然,拥有一个尾部递归函数的全部目的是表达一个循环(在函数样式中,是的),例如,在Common Lisp(在CLISP实现中)中,循环表达式

(loop for x in '(1 2) appending (list x))
(这是一种高层次的规范-y,如果它本身没有非常具体的功能的话)被转化为相同的跟踪和改变风格:

[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]>
[20]>(宏扩展“(12)追加(列表x)中x的循环)
(MACROLET((LOOP-FINISH NIL(系统::LOOP-FINISH-ERROR)))
(零区)
(让(#:G3047'(12)))
(项目
(让((X零))
(LET((#:ACCULIST-VAR-30483049零)(#:ACCULIST-VAR-3048零))
(MACROLET((循环-完成零’(GO系统::结束-循环)))
(标记体系统::开始循环(当(ENDP#:G3047)(循环结束))
(SETQ X(汽车:G3047))
(项目
(LET(#:G3050(副本列表(列表X)))
(如果#):ACCULIST-VAR-3048
(设置:ACCULIST-VAR-30483049
(最后(RPLACD#:ACCULIST-VAR-30483049#:G3050)))
(设置:ACCULIST-VAR-30483049
(最后(SETF#:ACCULIST-VAR-3048#:G3050()(#))))
(PSETQ:G3047(CDR:G3047))(GO系统::开始循环)系统::结束循环
(MACROLET)
((LOOP-FINISH NIL(系统::LOOP-FINISH-WARN)'(GO系统::END-LOOP)))
(从零返回:ACCULIST-VAR-3048(()()()()))));
T
[21]>

(所有结构变异原语的母亲拼写为
R.p.L.A.C.D.
),这是Lisp系统(不仅仅是Prolog)的一个例子,它实际上做了类似的事情。

你不能天真地看到,但也可以看到提供TCMC尾部调用模Cons的实现。允许

(cons head TAIL-EXPR)

如果cons本身是尾部调用,则执行尾部调用
tail-EXPR

这是一个使用continuations的函数式尾部递归追加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)))))))
从表现上看,在球拍和花招方面,这与威尔的突变型球拍和花招很接近,但在伊卡洛斯和鸡斯卡的反面表现更好。不过,变异总是表现最好的。然而,我不会使用这个,而是使用了Ó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))

对但之后你必须再次颠倒列表。TCMC的要点是,您不需要在最后再次反转列表,它提供了与命令式语言相同的性能。TCMC是一个相当不错的改进。@Vatine TRMC也可以被视为累加—它只是通过累加来累加。C可以做到,Scheme也可以做到。Prolog会自动进行优化。@WillNess AFAIK,无论何时使用
set
设置汽车
设置cdr这不再被认为是一个功能解决方案,在这种情况下,您正在更改state-cons单元格。我的代码不会改变任何外部程序可以观察到的任何状态。我已经重新阅读了Q,是的,我的实现不是Q所要求的“函数式实现”。正如您所指出的,如果没有内置的
snoc
操作,或者没有使用额外的
reverse
,这是无法做到的。对于为提高效率而编写的难以阅读的函数,您可以始终添加解释注释,如
foldr(:)[a]xs
,或
(设置cdr!(最后一对(复制列表xs))(列表a))
,或
=(反向)(cons elt(reverse lst))
,或者用英语。--自顶向下的O(1)额外空间TRMC代码的要点是,我们可以做得比将额外的O(n)增长堆栈结构替换为额外的O(n)增长延续结构更好,
((id.(1:)(2:)(3:)(4:)[5]
。实际上,要构建结果,这执行两个O(n)通过,而它的Haskell等价物执行三次,自上而下的TRMC代码执行一次。编译器实际上在后台执行突变,因此遗憾的是它没有像Prolog那样自动执行TRMC优化。如果您查看SRFI-1参考实现,您将看到所有有序过程都将
(define (reverse!-append-elt lst elt)
  (let ((lst (cons elt (reverse lst))))
     (reverse! lst)
     lst))