Queste altre risposte sono in qualche modo fuorvianti. Concordo sul fatto che dichiarano dettagli di attuazione che possono spiegare questa disparità, ma sopravvalutano il caso. Come giustamente suggerito da jmite, sono orientate all'attuazione verso rotti implementazioni di chiamate di funzione / ricorsione. Molte lingue implementano i loop attraverso la ricorsione, quindi i loop chiaramente non saranno più veloci in quelle lingue. In teoria la ricorsione non è in alcun modo meno efficace del looping (quando entrambi sono applicabili). Permettetemi di citare l'abstract sul documento di Guy Steele del 1977 Debunking il mito "Expensive Procedure Call" o, Implementazioni di procedura considerate dannose o, Lambda: il GOTO definitivo
Il folklore afferma che le dichiarazioni GOTO sono "economiche", mentre le chiamate di procedura sono "costose". Questo mito è in gran parte il risultato di implementazioni linguistiche mal progettate. La crescita storica di questo mito è considerata. Vengono discusse sia idee teoriche sia un'implementazione esistente che sfatano questo mito. È dimostrato che l'uso senza restrizioni delle chiamate di procedura consente una grande libertà stilistica. In particolare, qualsiasi diagramma di flusso può essere scritto come un programma "strutturato" senza introdurre variabili aggiuntive. La difficoltà con l'istruzione GOTO e la chiamata alla procedura è caratterizzata da un conflitto tra concetti di programmazione astratti e costrutti di linguaggio concreti.
Il "conflitto tra concetti di programmazione astratti e costrutti di linguaggio concreti" può essere visto dal fatto che la maggior parte dei modelli teorici, ad esempio, del calcolo lambda non tipizzato , non hanno una pila . Ovviamente, questo conflitto non è necessario come illustra il documento sopra e come è dimostrato anche da lingue che non hanno meccanismi di iterazione diversi dalla ricorsione come Haskell.
fix
fix f x = f (fix f) x
( λ x . M) N⇝ M[ N/ x][ N/ x]XMN⇝
Ora per un esempio. Definisci fact
come
fact = fix (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) 1
Ecco la valutazione di fact 3
dove, per compattezza, userò g
come sinonimo di fix (λf.λa.λn.if n == 0 then a else f (a*n) (n-1))
, cioè fact = g 1
. Questo non influisce sul mio argomento.
fact 3
~> g 1 3
~> fix (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) 1 3
~> (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) g 1 3
~> (λa.λn.if n == 0 then a else g (a*n) (n-1)) 1 3
~> (λn.if n == 0 then 1 else g (1*n) (n-1)) 3
~> if 3 == 0 then 1 else g (1*3) (3-1)
~> g (1*3) (3-1)
~> g 3 2
~> fix (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) 3 2
~> (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) g 3 2
~> (λa.λn.if n == 0 then a else g (a*n) (n-1)) 3 2
~> (λn.if n == 0 then 3 else g (3*n) (n-1)) 2
~> if 2 == 0 then 3 else g (3*2) (2-1)
~> g (3*2) (2-1)
~> g 6 1
~> fix (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) 6 1
~> (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) g 6 1
~> (λa.λn.if n == 0 then a else g (a*n) (n-1)) 6 1
~> (λn.if n == 0 then 6 else g (6*n) (n-1)) 1
~> if 1 == 0 then 6 else g (6*1) (1-1)
~> g (6*1) (1-1)
~> g 6 0
~> fix (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) 6 0
~> (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) g 6 0
~> (λa.λn.if n == 0 then a else g (a*n) (n-1)) 6 0
~> (λn.if n == 0 then 6 else g (6*n) (n-1)) 0
~> if 0 == 0 then 6 else g (6*0) (0-1)
~> 6
Puoi vedere dalla forma senza nemmeno guardare i dettagli che non c'è crescita e che ogni iterazione necessita della stessa quantità di spazio. (Tecnicamente, il risultato numerico cresce, il che è inevitabile e altrettanto vero per un while
ciclo.) Ti sfido a sottolineare lo "stack" senza limiti di crescita qui.
Sembra che la semantica archetipica del calcolo lambda faccia già quello che viene comunemente chiamato "ottimizzazione della coda". Naturalmente, qui non sta avvenendo alcuna "ottimizzazione". Non ci sono regole speciali qui per le chiamate "di coda" rispetto alle chiamate "normali". Per questo motivo, è difficile dare una caratterizzazione "astratta" di ciò che sta facendo l'ottimizzazione delle chiamate di coda, come in molte caratterizzazioni astratte della semantica delle chiamate di funzione, non c'è nulla da fare per "l'ottimizzazione" delle chiamate di coda!
Che la definizione analoga di fact
"overflow dello stack" in molte lingue, è un fallimento da parte di tali lingue nell'implementare correttamente la semantica della chiamata di funzione. (Alcune lingue hanno una scusa.) La situazione è all'incirca analoga all'avere un'implementazione del linguaggio che implementa array con elenchi collegati. L'indicizzazione in tali "matrici" sarebbe quindi un'operazione O (n) che non soddisfa le aspettative delle matrici. Se avessi realizzato un'implementazione separata del linguaggio, che utilizzava array reali anziché elenchi collegati, non diresti che ho implementato "ottimizzazione dell'accesso agli array", diresti che ho risolto un'implementazione non corretta degli array.
Quindi, rispondendo alla risposta di Veedrac. Le pile non sono "fondamentali" per la ricorsione . Nella misura in cui si verifica un comportamento "simile a stack" nel corso della valutazione, ciò può avvenire solo nei casi in cui i loop (senza una struttura di dati ausiliaria) non sarebbero applicabili in primo luogo! Per dirla in altro modo, posso implementare loop con ricorsione con esattamente le stesse caratteristiche prestazionali. In effetti, Scheme e SML contengono entrambi costrutti loop, ma entrambi definiscono quelli in termini di ricorsione (e, almeno in Scheme, do
sono spesso implementati come una macro che si espande in chiamate ricorsive). Analogamente, per la risposta di Johan, nulla dice un il compilatore deve emettere l'assemblaggio che Johan ha descritto per la ricorsione. Infatti,esattamente lo stesso assemblaggio se si utilizzano loop o ricorsioni. L'unica volta in cui il compilatore sarebbe (in qualche modo) obbligato a emettere assembly come quello che Johan descrive è quando stai facendo qualcosa che non è comunque esprimibile da un loop. Come indicato nel documento di Steele e dimostrato dalla pratica di linguaggi come Haskell, Scheme e SML, non è "estremamente raro" che le chiamate di coda possano essere "ottimizzate", possono sempreessere "ottimizzato". Se un uso particolare della ricorsione verrà eseguito in uno spazio costante dipende da come viene scritto, ma le restrizioni che è necessario applicare per renderlo possibile sono le restrizioni necessarie per adattare il problema alla forma di un ciclo. (In realtà, sono meno rigorosi. Ci sono problemi, come la codifica delle macchine a stati, che sono gestiti in modo più pulito ed efficiente tramite chiamate di coda rispetto ai loop che richiederebbero variabili ausiliarie.) Ancora una volta, l'unica volta che la ricorsione richiede di fare più lavoro è quando il tuo codice non è un loop comunque.
La mia ipotesi è che Johan si riferisca ai compilatori C che hanno restrizioni arbitrarie su quando eseguirà "l'ottimizzazione" delle chiamate di coda. Anche presumibilmente Johan si riferisce a linguaggi come C ++ e Rust quando parla di "linguaggi con tipi gestiti". Il linguaggio RAII del C ++ e presente anche in Rust rende le cose che sembrano superficialmente chiamate di coda, non chiamate di coda (perché i "distruttori" devono ancora essere chiamati). Ci sono state proposte per utilizzare una sintassi diversa per optare per una semantica leggermente diversa che consentirebbe la ricorsione della coda (cioè prima di chiamare i distruttoril'ultima chiamata finale e ovviamente non consente l'accesso agli oggetti "distrutti"). (La garbage collection non presenta questo problema, e tutti gli Haskell, SML e Scheme sono linguaggi di garbage collection.) In una vena abbastanza diversa, alcune lingue, come Smalltalk, espongono lo "stack" come oggetto di prima classe, in questi casi lo "stack" non è più un dettaglio di implementazione, sebbene ciò non precluda la presenza di tipi separati di chiamate con semantica diversa. (Java dice che non può a causa del modo in cui gestisce alcuni aspetti della sicurezza, ma questo è in realtà falso .)
In pratica, la prevalenza di implementazioni errate di chiamate di funzioni deriva da tre fattori principali. Innanzitutto, molte lingue ereditano l'implementazione non corretta dal loro linguaggio di implementazione (di solito C). In secondo luogo, la gestione delle risorse deterministiche è piacevole e rende il problema più complicato, sebbene solo una manciata di lingue lo offra. In terzo luogo, e, nella mia esperienza, il motivo per cui la maggior parte delle persone si preoccupa, è che vogliono tracce dello stack quando si verificano errori per scopi di debug. Solo la seconda ragione è quella che può essere potenzialmente teoricamente motivata.