Odpowiedz

4

Dobrze jest można napisać procedurę ogon rekurencyjnej 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))))) 

... ale to bardziej nieefektywne z tego reverse wrzucony (na wszelki wypadek). Nie mogę wymyślić innego funkcjonalnego (np. Bez modyfikowania listy wejściowej) sposobu napisania tej procedury jako rekursji ogonowej bez wcześniejszego odwrócenia listy.

Dla niefunkcjonującej odpowiedź na to pytanie, @WillNess dostarczyła przyjemne rozwiązanie Scheme mutujące listę wewnętrzną.

+0

@WillNess AFAIK, gdy używasz 'ustawić', 'set-samochód', 'set-CDR' to nie jest już uważane za rozwiązanie funkcjonalne, w tym przypadku zmienia się stan - komórki cons. Nawet jeśli wydaje się funkcjonalny na zewnątrz. –

+0

mój kod nie mutuje żadnego stanu obserwowalnego przez żaden program zewnętrzny. I nie mutuje listy wejściowej w miejscu. –

+0

Przeczytałem ponownie Q, tak, moja implementacja nie jest "implementacją stylu funkcjonalnego **", o którą pyta Q. Nie da się tego zrobić bez wbudowanej operacji 'snoc' lub użycia dodatkowego' reverse ', jak zauważyłeś. –

1

To Lisp, nie Scheme, ale jestem pewien, że można przetłumaczyć:

(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))) 

trzymam głowę i ogon listy powrotnej i modyfikować ogon jak przemierzać lista jej argumentów.

6

Poniżej przedstawiono implementację optymalizacji tail recursion modulo cons, w wyniku której uzyskano w pełni ogonowy kod rekurencyjny. Kopiuje strukturę wejściową, a następnie dołącza do niej nowy element, poprzez mutację, w sposób odgórny. Ponieważ mutacja ta jest wykonywana na jego wewnętrznych świeżo utworzonych danych, jest nadal funkcjonalny na zewnątrz (nie zmienia żadnych danych przekazywanych do niego i nie ma skutków obserwowalne wyjątkiem wytwarzania jej wynik):

(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))))))) 

lubię używając sztuczki "head-sentinel", znacznie upraszcza kod kosztem alokacji tylko jednej dodatkowej komórki.

Ten kod używa prymitywów mutacji niskiego poziomu, aby osiągnąć to, co w niektórych językach (np. Prolog) jest wykonywane automatycznie przez kompilator. W TRMC optymalizacja hipotetycznego projektu, będziemy mogli napisać następujące ogon rekurencyjnej modulo minusy kod i mieć kompilator automatycznie przetłumaczyć je na jakiś odpowiednik powyższego kodu:

(define (append-elt lst elt)    ;; %% in Prolog: 
    (if (null lst)       ;; app([], X, Z) :- Z=[X]. 
    (list elt)       ;; app([A|B], X, Z) :- 
    (cons (car lst)      ;; Z=[A|Y],   % cons _before_ 
      (append-elt (cdr lst) elt)))) ;; app(B, X, Y). % tail call 

Jeśli nie dla cons operacja, append-eltbędzie być rekurencyjne. Tutaj zaczyna działać optymalizacja TRMC.

2

Nie można naiwnie, ale zobacz także implementacje zapewniające TCMC - Tail Call Modulo Cons. To pozwala

(cons head TAIL-EXPR) 

do ogona połączenia TAIL-EXPR Jeżeli sam wad jest ogon połączenia.

+0

Tak; ale potem musisz ponownie odwrócić listę. Celem TCMC jest to, że nie musisz ponownie odwracać listy na końcu i daje to taką samą wydajność, jaką możesz uzyskać z imperatywnych języków. – LeoNerd

+0

Tak, TCMC jest raczej czystym ulepszeniem. – Vatine

+0

@Vatine TRMC może również być postrzegany jako akumulujący - gromadzi się po prostu przez nconcing. C może to zrobić, a Schemat też to potrafi. Prolog automatycznie przeprowadza optymalizację. –

2

Jest to funkcjonalny, ogon rekurencyjnej append-ELT użyciu kontynuacje:

(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))))))) 

Performance-mądry to blisko Will mutacji jeden w Racket i Gambit ale w Ikarus i reverse Chicken Oscar jest nie lepiej. Mutacja zawsze była jednak najlepsza. Nie skorzystałbym jednak z tego, ale niewielka wersja zapisu Óscar, ponieważ jest łatwiejsza do odczytania.

(define (reverse-append-elt lst elt) 
    (reverse (cons elt (reverse lst)))) 

A jeśli chcesz mutacji wydajność bym zrobiła!!!

(define (reverse!-append-elt lst elt) 
    (let ((lst (cons elt (reverse lst)))) 
    (reverse! lst) 
    lst)) 
+0

dla funkcji trudnych do odczytania napisanych dla zwiększenia efektywności, zawsze możesz dodać komentarze wyjaśniające, takie jak 'foldr (:) [a] xs' lub' (set-cdr! (Last-pair (copy-list xs)) (list a)) ', lub' == (reverse (cons elt (reverse lst))) lub w języku angielskim. - Punkt skierowany do O-1 z dodatkowym kosmicznym kodem TRMC jest lepszy niż wymiana dodatkowej O (n) struktury rosnącej stosu dla dodatkowej struktury rosnącej o (n), '(((((1:)). (2:)). (3:)). (4 :)) [5] '. W efekcie, aby zbudować wynik, wykonuje dwa przejścia O (n), podczas gdy jego odpowiednik Haskella ma trzy, a kod TRMC z góry do dołu - jedno przejście. –

+0

Kompilatory faktycznie dokonują mutacji pod maską, więc jest to smutne, że nie przeprowadza automatycznie optymalizacji TRMC, tak jak robi to Prolog. Jeśli spojrzysz na implementację referencyjną SRFI-1, zobaczysz, że wszystkie procedury porządkowe wysadzą stos, dając wystarczająco dużą listę. – Sylwester

+0

Tak, zawsze uważałem to za bardzo dziwne. Może podają tę referencyjną implementację tak jak * opis * tego, jakie wyniki muszą mieć; a * specyfikacja *. --- Btw TRMC jest łatwe do zobaczenia/prezentowania jako rekursja ogona z akumulatorem; właśnie ta akumulacja jest dokonywana przez ustawianie cdr ostatniej pary. –

Powiązane problemy