Come viene memorizzata questa funzione di Fibonacci?


114

Con quale meccanismo viene memorizzata questa funzione di Fibonacci?

fib = (map fib' [0..] !!)                 
     where fib' 1 = 1                                                        
           fib' 2 = 1                                                        
           fib' n = fib (n-2) + fib (n-1)                    

E in una nota correlata, perché questa versione non lo è?

fib n = (map fib' [0..] !! n)                                               
     where fib' 1 = 1                                                        
           fib' 2 = 1                                                        
           fib' n = fib (n-2) + fib (n-1)                    

13
Leggermente non correlato, fib 0non termina: probabilmente vuoi che i casi base fib'siano fib' 0 = 0e fib' 1 = 1.
huon

1
Si noti che la prima versione potrebbe essere resa più concisa: fibs = 1:1:zipWith (+) fibs (tail fibs)e fib = (fibs !!).
Bastian

Risposte:


95

Il meccanismo di valutazione in Haskell è per necessità : quando un valore è necessario, viene calcolato e tenuto pronto nel caso venga richiesto di nuovo. Se definiamo una lista, xs=[0..]e in seguito chiediamo il suo centesimo elemento, xs!!99il 100esimo slot nell'elenco viene "arricchito", tenendo il numero 99ora, pronto per il prossimo accesso.

Questo è ciò che sta sfruttando quel trucco, "sfogliare una lista". Nella normale definizione di Fibonacci doppiamente ricorsiva fib n = fib (n-1) + fib (n-2), la funzione stessa viene chiamata, due volte dall'alto, provocando l'esplosione esponenziale. Ma con questo trucco, abbiamo stabilito un elenco per i risultati provvisori e passiamo "attraverso l'elenco":

fib n = (xs!!(n-1)) + (xs!!(n-2)) where xs = 0:1:map fib [2..]

Il trucco sta nel far sì che quella lista venga creata e che la lista non vada via (tramite garbage collection) tra le chiamate a fib. Il modo più semplice per ottenere ciò è nominare tale elenco. "Se lo chiami, rimarrà."


La tua prima versione definisce una costante monomorfica e la seconda definisce una funzione polimorfica. Una funzione polimorfica non può utilizzare lo stesso elenco interno per diversi tipi che potrebbe dover servire, quindi nessuna condivisione , ovvero nessuna memorizzazione.

Con la prima versione, il compilatore è generoso con noi, eliminando quella sottoespressione costante ( map fib' [0..]) e rendendola un'entità condivisibile separata, ma non è obbligato a farlo. e ci sono casi in cui non vogliamo che lo faccia automaticamente per noi.

( modifica :) Considera queste riscritture:

fib1 = f                     fib2 n = f n                 fib3 n = f n          
 where                        where                        where                
  f i = xs !! i                f i = xs !! i                f i = xs !! i       
  xs = map fib' [0..]          xs = map fib' [0..]          xs = map fib' [0..] 
  fib' 1 = 1                   fib' 1 = 1                   fib' 1 = 1          
  fib' 2 = 1                   fib' 2 = 1                   fib' 2 = 1          
  fib' i=fib1(i-2)+fib1(i-1)   fib' i=fib2(i-2)+fib2(i-1)   fib' i=f(i-2)+f(i-1)

Quindi la vera storia sembra riguardare le definizioni di ambito annidato. Non esiste uno scopo esterno con la prima definizione e il terzo sta attento a non chiamare l'ambito esterno fib3, ma lo stesso livello f.

Ogni nuova invocazione di fib2sembra creare di nuovo le sue definizioni annidate perché ognuna di esse potrebbe (in teoria) essere definita in modo diverso a seconda del valore di n(grazie a Vitus e Tikhon per averlo sottolineato). Con la prima definizione non c'è niente nda cui dipendere, e con la terza c'è una dipendenza, ma ogni chiamata separata alle fib3chiamate in fcui è attenta a chiamare solo definizioni dallo stesso livello di ambito, interno a questa specifica invocazione di fib3, quindi lo stesso xsottiene riutilizzato (cioè condiviso) per quell'invocazione di fib3.

Ma nulla impedisce al compilatore di riconoscere che le definizioni interne in una qualsiasi delle versioni precedenti sono in realtà indipendenti dal nbinding esterno , per eseguire dopotutto il sollevamento lambda , risultando in piena memoizzazione (eccetto per le definizioni polimorfiche). In effetti è esattamente ciò che accade con tutte e tre le versioni quando dichiarate con tipi monomorfici e compilate con il flag -O2. Con dichiarazioni di tipo polimorfico, fib3mostra condivisione locale e fib2nessuna condivisione.

In definitiva, a seconda del compilatore e delle ottimizzazioni del compilatore utilizzate, e di come lo testate (caricamento di file in GHCI, compilato o meno, con -O2 o no, o autonomo), e se ottiene un tipo monomorfico o polimorfico il comportamento potrebbe cambia completamente - se mostra condivisione locale (per chiamata) (cioè tempo lineare su ogni chiamata), memoizzazione (cioè tempo lineare alla prima chiamata e tempo 0 nelle chiamate successive con argomenti uguali o inferiori) o nessuna condivisione ( tempo esponenziale).

La risposta breve è, è una cosa del compilatore. :)


4
Giusto per correggere un piccolo dettaglio: la seconda versione non ottiene alcuna condivisione principalmente perché la funzione locale fib'è ridefinita per ogni ne quindi fib'in fib 1fib'in fib 2, il che implica anche che le liste sono diverse. Anche se si fissa il tipo in modo che sia monomorfo, mostra comunque questo comportamento.
Vitus

1
wherele clausole introducono la condivisione in modo molto simile a letespressioni, ma tendono a nascondere problemi come questo. Riscrivendolo un po 'più esplicitamente, ottieni questo: hpaste.org/71406
Vitus

1
Un altro punto interessante sulla tua riscrittura: se dai loro il tipo monomorfo (cioè Int -> Integer), allora fib2viene eseguito in tempo esponenziale, fib1ed fib3entrambi vengono eseguiti in tempo lineare ma fib1viene anche memorizzato - ancora una volta perché per fib3le definizioni locali sono ridefinite per ogni n.
Vitus

1
@misterbee Ma in effetti sarebbe bello avere una sorta di sicurezza dal compilatore; una sorta di controllo sulla residenza della memoria di un'entità specifica. A volte vogliamo la condivisione, a volte vogliamo impedirla. Immagino / spero che sia possibile ...
Will Ness

1
@ElizaBrandt quello che volevo dire era che a volte vogliamo ricalcolare qualcosa di pesante in modo che non venga conservato per noi in memoria, ovvero il costo del ricalcolo è inferiore al costo di un'enorme conservazione della memoria. un esempio è la creazione del PowerSet: pwr (x:xs) = pwr xs ++ map (x:) pwr xs ; pwr [] = [[]]vogliamo pwr xsche sia calcolato in modo indipendente, due volte, in modo che possa essere raccolto al volo mentre viene prodotto e consumato.
Will Ness

23

Non ne sono del tutto certo, ma ecco un'ipotesi plausibile:

Il compilatore presume che fib npotrebbe essere diverso su un diverso ne quindi dovrebbe ricalcolare l'elenco ogni volta. Dopotutto, i bit all'interno wheredell'istruzione potrebbero dipendere n. Cioè, in questo caso, l'intero elenco di numeri è essenzialmente una funzione di n.

La versione senza n può creare l'elenco una volta e racchiuderlo in una funzione. L'elenco non può dipendere dal valore di npassato e questo è facile da verificare. L'elenco è una costante che viene quindi indicizzata. È, ovviamente, una costante che viene valutata pigramente, quindi il tuo programma non cerca di ottenere immediatamente l'intero elenco (infinito). Poiché è una costante, può essere condivisa tra le chiamate di funzione.

È memorizzato del tutto perché la chiamata ricorsiva deve solo cercare un valore in un elenco. Poiché la fibversione crea l'elenco una volta pigramente, calcola solo quanto basta per ottenere la risposta senza eseguire calcoli ridondanti. Qui, "pigro" significa che ogni voce nell'elenco è un thunk (un'espressione non valutata). Quando si fa a valutare il thunk, diventa un valore, in modo da accedervi prossima volta non fa ripetere il calcolo. Poiché l'elenco può essere condiviso tra le chiamate, tutte le voci precedenti sono già calcolate dal momento in cui è necessaria quella successiva.

È essenzialmente una forma intelligente ea basso costo di programmazione dinamica basata sulla semantica pigra di GHC. Penso che lo standard specifichi solo che deve essere non rigoroso , quindi un compilatore conforme potrebbe potenzialmente compilare questo codice per non memorizzare. Tuttavia, in pratica, ogni compilatore ragionevole sarà pigro.

Per ulteriori informazioni sul motivo per cui il secondo caso funziona, leggi Comprensione di un elenco definito ricorsivamente (bugie in termini di zipWith) .


volevi dire " fib' npotrebbe essere diverso su un diverso n" forse?
Will Ness

Penso di non essere stato molto chiaro: quello che volevo dire era che tutto dentro fib, incluso fib', poteva essere diverso in ogni diverso n. Penso che l'esempio originale sia un po 'confuso perché fib'dipende anche dal suo nche ombreggia l'altro n.
Tikhon Jelvis

20

Innanzitutto, con ghc-7.4.2, compilato con -O2, la versione senza memo non è poi così male, l'elenco interno dei numeri di Fibonacci è ancora memorizzato per ogni chiamata di primo livello alla funzione. Ma non è e non può ragionevolmente essere memorizzato in diverse chiamate di primo livello. Tuttavia, per l'altra versione, l'elenco è condiviso tra le chiamate.

Ciò è dovuto alla restrizione del monomorfismo.

Il primo è vincolato da un semplice pattern binding (solo il nome, nessun argomento), quindi dalla restrizione del monomorfismo deve ottenere un tipo monomorfo. Il tipo dedotto è

fib :: (Num n) => Int -> n

e tale vincolo viene impostato come predefinito (in assenza di una dichiarazione predefinita che dica diversamente) Integer, fissando il tipo come

fib :: Int -> Integer

Quindi c'è solo una lista (di tipo [Integer]) da memorizzare.

Il secondo è definito con un argomento funzione, quindi rimane polimorfico, e se gli elenchi interni fossero memorizzati tra le chiamate, dovrebbe essere memorizzato un elenco per ogni tipo in Num. Non è pratico.

Compilare entrambe le versioni con la restrizione del monomorfismo disabilitata o con firme di tipo identico ed entrambe mostrano esattamente lo stesso comportamento. (Questo non era vero per le versioni precedenti del compilatore, non so quale versione lo facesse per prima.)


Perché non è pratico memorizzare un elenco per ogni tipo? In linea di principio, GHC potrebbe creare un dizionario (molto simile alla chiamata di funzioni vincolate alla classe di tipo) per contenere elenchi parzialmente calcolati per ogni tipo Num incontrato durante il runtime?
misterbee

1
@misterbee In linea di principio, potrebbe, ma se il programma richiede fib 1000000molti tipi, consuma un sacco di memoria. Per evitare ciò, sarebbe necessaria un'euristica che elenchi da eliminare dalla cache quando diventa troppo grande. E una tale strategia di memorizzazione si applicherebbe anche ad altre funzioni o valori, presumibilmente, quindi il compilatore dovrebbe occuparsi di un numero potenzialmente elevato di cose da memorizzare per potenzialmente molti tipi. Penso che sarebbe possibile implementare la memoria polimorfica (parziale) con un'euristica ragionevolmente buona, ma dubito che ne varrebbe la pena.
Daniel Fischer

5

Non hai bisogno della funzione memoize per Haskell. Solo il linguaggio di programmazione empirativo ha bisogno di queste funzioni. Tuttavia, Haskel è lang funzionale e ...

Quindi, questo è un esempio di algoritmo di Fibonacci molto veloce:

fib = zipWith (+) (0:(1:fib)) (1:fib)

zipCon la sua funzione dallo standard Prelude:

zipWith :: (a->b->c) -> [a]->[b]->[c]
zipWith op (n1:val1) (n2:val2) = (n1 + n2) : (zipWith op val1 val2)
zipWith _ _ _ = []

Test:

print $ take 100 fib

Produzione:

[1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597,2584,4181,6765,10946,17711,28657,46368,75025,121393,196418,317811,514229,832040,1346269,2178309,3524578,5702887,9227465,14930352,24157817,39088169,63245986,102334155,165580141,267914296,433494437,701408733,1134903170,1836311903,2971215073,4807526976,7778742049,12586269025,20365011074,32951280099,53316291173,86267571272,139583862445,225851433717,365435296162,591286729879,956722026041,1548008755920,2504730781961,4052739537881,6557470319842,10610209857723,17167680177565,27777890035288,44945570212853,72723460248141,117669030460994,190392490709135,308061521170129,498454011879264,806515533049393,1304969544928657,2111485077978050,3416454622906707,5527939700884757,8944394323791464,14472334024676221,23416728348467685,37889062373143906,61305790721611591,99194853094755497,160500643816367088,259695496911122585,420196140727489673,679891637638612258,1100087778366101931,1779979416004714189,2880067194370816120,4660046610375530309,7540113804746346429,12200160415121876738,19740274219868223167,31940434634990099905,51680708854858323072,83621143489848422977,135301852344706746049,218922995834555169026,354224848179261915075,573147844013817084101]

Tempo trascorso: 0.00018 s


Questa soluzione è fantastica!
Larry
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.