【问题标题】:tail-recursive function appending element to list尾递归函数将元素附加到列表
【发布时间】:2012-10-26 16:23:56
【问题描述】:

我见过几个将append 元素实现到列表的示例,但都没有使用尾递归。如何在函数式风格中实现这样的功能?

 (define (append-list lst elem)
    expr)

【问题讨论】:

    标签: lisp scheme racket tail-call-optimization tailrecursion-modulo-cons


    【解决方案1】:

    嗯,可以编写一个尾递归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 解决方案来改变内部列表。

    【讨论】:

    • @WillNess AFAIK,每当您使用 set!set-car!set-cdr! 这不再被视为功能解决方案时,您正在更改状态 - 在这种情况下是 cons 单元格。即使它在外面看起来很实用。
    • 我的代码不会改变任何外部程序可观察到的任何状态。而且它不会就地改变输入列表。
    • 我重新阅读了 Q,是的,我的实现不是 Q 询问的“功能样式实现”。正如您所指出的,如果没有内置的snoc 操作或使用额外的reverse,则无法做到这一点。
    【解决方案2】:

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

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

    【讨论】:

      【解决方案3】:

      以下是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)的一个例子。

      【讨论】:

        【解决方案4】:

        您不能天真,但另请参阅提供 TCMC - Tail Call Modulo Cons 的实现。这允许

        (cons head TAIL-EXPR)
        

        如果 cons 本身是尾调用,则尾调用 TAIL-EXPR

        【讨论】:

        • 是的;但是之后您必须再次反转列表。 TCMC 的重点是您不需要在最后再次反转列表,它提供了与命令式语言相同的性能
        • 是的,TCMC 是一个相当巧妙的改进。
        • @Vatine TRMC 也可以被视为积累——它只是通过 nconcing 积累。 C 可以做到,Scheme 也可以。 Prolog 会自动进行优化。
        【解决方案5】:

        这是一个使用延续的函数式尾递归追加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))
        

        【讨论】:

        • 对于为了提高效率而编写的难以阅读的函数,您可以随时添加解释 cmets,如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 代码 - 一遍。
        • 编译器实际上在后台进行了突变,所以很遗憾它没有像 Prolog 那样自动进行 TRMC 优化。如果您查看 SRFI-1 参考实现,您会发现所有有序过程都会在列表足够大的情况下炸毁堆栈。
        • 是的,我一直觉得这很奇怪。也许他们给出这个参考实现只是作为结果必须是什么的描述规范。 --- 顺便说一句,TRMC 很容易看到/呈现为带有累加器的尾递归;只是这种累积是通过 set-cdr!ing 最后一对来完成的。
        猜你喜欢
        • 1970-01-01
        • 2018-04-08
        • 1970-01-01
        • 1970-01-01
        • 2020-12-19
        • 1970-01-01
        • 2014-06-10
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多