comportamento foldl contro foldr con elenchi infiniti


124

Il codice per la funzione myAny in questa domanda usa foldr. Arresta l'elaborazione di un elenco infinito quando il predicato è soddisfatto.

L'ho riscritto usando foldl:

myAny :: (a -> Bool) -> [a] -> Bool
myAny p list = foldl step False list
   where
      step acc item = p item || acc

(Notare che gli argomenti della funzione step sono correttamente invertiti.)

Tuttavia, non interrompe più l'elaborazione di elenchi infiniti.

Ho tentato di tracciare l'esecuzione della funzione come nella risposta di Apocalisp :

myAny even [1..]
foldl step False [1..]
step (foldl step False [2..]) 1
even 1 || (foldl step False [2..])
False  || (foldl step False [2..])
foldl step False [2..]
step (foldl step False [3..]) 2
even 2 || (foldl step False [3..])
True   || (foldl step False [3..])
True

Tuttavia, questo non è il modo in cui si comporta la funzione. Come è sbagliato?

Risposte:


231

Il modo in cui folddifferiscono sembra essere una frequente fonte di confusione, quindi ecco una panoramica più generale:

Considera la possibilità di piegare un elenco di n valori [x1, x2, x3, x4 ... xn ]con una funzione fe un seme z.

foldl è:

  • Associativo di sinistra :f ( ... (f (f (f (f z x1) x2) x3) x4) ...) xn
  • Ricorsivo di coda : itera l'elenco, producendo il valore in seguito
  • Pigro : non viene valutato nulla finché non è necessario il risultato
  • Indietro : foldl (flip (:)) []inverte un elenco.

foldr è:

  • Diritto associativo :f x1 (f x2 (f x3 (f x4 ... (f xn z) ... )))
  • Ricorsivo in un argomento : ogni iterazione si applica fal valore successivo e al risultato del ripiegamento del resto della lista.
  • Pigro : non viene valutato nulla finché non è necessario il risultato
  • Avanti : foldr (:) []restituisce una lista invariata.

C'è un punto leggermente sottile qui che a volte fa inciampare le persone: poiché foldlè al contrario, ogni applicazione di fviene aggiunta all'esterno del risultato; e poiché è pigro , non viene valutato nulla finché non viene richiesto il risultato. Ciò significa che per calcolare qualsiasi parte del risultato, Haskell esegue prima l'iterazione dell'intero elenco costruendo un'espressione di applicazioni di funzioni annidate, quindi valuta la funzione più esterna , valutando i suoi argomenti secondo necessità. Se fusa sempre il suo primo argomento, significa che Haskell deve ricorrere fino al termine più interno, quindi lavorare a ritroso calcolando ogni applicazione di f.

Questo è ovviamente molto diverso dall'efficiente ricorsione della coda che i programmatori più funzionali conoscono e amano!

In effetti, anche se foldltecnicamente è ricorsivo alla coda, poiché l'intera espressione del risultato è costruita prima di valutare qualsiasi cosa, foldlpuò causare un overflow dello stack!

D'altra parte, considera foldr. È anche pigro, ma poiché viene eseguito in avanti , ogni applicazione di fviene aggiunta all'interno del risultato. Quindi, per calcolare il risultato, Haskell costruisce un'applicazione a funzione singola , il cui secondo argomento è il resto della lista piegata. Se fè pigro nel suo secondo argomento, ad esempio un costruttore di dati, il risultato sarà incrementalmente pigro , con ogni passo della piega calcolato solo quando una parte del risultato che lo richiede viene valutata.

Quindi possiamo vedere perché a foldrvolte funziona su elenchi infiniti quando foldlnon lo fa: il primo può convertire pigramente un elenco infinito in un'altra struttura dati infinita pigra, mentre il secondo deve ispezionare l'intero elenco per generare qualsiasi parte del risultato. D'altra parte, foldrcon una funzione che necessita di entrambi gli argomenti immediatamente, come (+), funziona (o meglio, non funziona) in modo molto simile foldl, costruire un'espressione enorme prima di valutarla.

Quindi i due punti importanti da notare sono questi:

  • foldr può trasformare una struttura dati ricorsiva pigra in un'altra.
  • In caso contrario, le pieghe pigre si bloccheranno con un overflow dello stack su elenchi grandi o infiniti.

Potresti aver notato che sembra che si foldrpossa fare tutto il foldlpossibile, e anche di più. Questo è vero! In effetti, foldl è quasi inutile!

Ma cosa succede se vogliamo produrre un risultato non pigro ripiegando su un elenco ampio (ma non infinito)? Per questo, vogliamo una piega rigorosa , che le librerie standard forniscono con attenzione :

foldl' è:

  • Associativo di sinistra :f ( ... (f (f (f (f z x1) x2) x3) x4) ...) xn
  • Ricorsivo di coda : itera l'elenco, producendo il valore in seguito
  • Rigido : ogni applicazione di funzione viene valutata lungo il percorso
  • Indietro : foldl' (flip (:)) []inverte un elenco.

Poiché foldl'è rigoroso , per calcolare il risultato Haskell valuterà f ad ogni passaggio, invece di lasciare che l'argomento a sinistra accumuli un'espressione enorme e non valutata. Questo ci dà la solita, efficiente ricorsione della coda che vogliamo! In altre parole:

  • foldl' può piegare in modo efficiente elenchi di grandi dimensioni.
  • foldl' si bloccherà in un ciclo infinito (non causerà un overflow dello stack) su un elenco infinito.

Anche il wiki Haskell ha una pagina che ne discute .


6
Sono venuto qui perché sono curioso di sapere perché foldrè meglio che foldlin Haskell , mentre è vero il contrario a Erlang (che ho imparato prima di Haskell ). Poiché Erlang non è pigro e le funzioni non sono curry , così foldlin Erlang si comporta come foldl'sopra. Questa è un'ottima risposta! Buon lavoro e grazie!
Siu Ching Pong -Asuka Kenji-

7
Questa è principalmente un'ottima spiegazione, ma trovo problematica la descrizione di foldlcome "indietro" e foldrcome "avanti". Ciò è in parte dovuto al fatto che flipviene applicato (:)nell'illustrazione del motivo per cui fold è all'indietro. La reazione naturale è "ovviamente è all'indietro: hai fatto la fliplista di concatenazione!" È anche strano vedere quello chiamato "all'indietro" poiché si foldlapplica fal primo elemento della lista per primo (più interno) in una valutazione completa. È foldrche "corre all'indietro", applicandosi fper primo all'ultimo elemento.
Dave Abrahams

1
@DaveAbrahams: Tra giusto foldle foldrignorando rigore e ottimizzazioni, prima significa "più esterno", non "più interno". Questo è il motivo per cui foldrpuò elaborare liste infinite e foldlnon può: la piega a destra si applica prima fal primo elemento della lista e al risultato (non valutato) della piegatura della coda, mentre la piega a sinistra deve attraversare l'intera lista per valutare l'applicazione più esterna di f.
CA McCann

1
Mi chiedo solo se c'è qualche istanza in cui foldl sarebbe preferito rispetto a foldl ', pensi che ce ne sia uno?
kazuoua

1
@kazuoua dove la pigrizia è essenziale, ad es last xs = foldl (\a z-> z) undefined xs.
Will Ness

28
myAny even [1..]
foldl step False [1..]
foldl step (step False 1) [2..]
foldl step (step (step False 1) 2) [3..]
foldl step (step (step (step False 1) 2) 3) [4..]

eccetera.

Intuitivamente, foldlè sempre "fuori" o "sinistra", quindi viene espanso per primo. Verso l'infinito.


10

Puoi vedere nella documentazione di Haskell qui che foldl è ricorsivo di coda e non finirà mai se viene passato un elenco infinito, poiché chiama se stesso al parametro successivo prima di restituire un valore ...


0

Non conosco Haskell, ma in Scheme fold-right"agirà" sempre per primo sull'ultimo elemento di una lista. Quindi non funzionerà per l'elenco ciclico (che è uguale a quello infinito).

Non sono sicuro che fold-rightpossa essere scritto ricorsivo in coda, ma per qualsiasi elenco ciclico dovresti ottenere uno stack overflow. fold-leftOTOH è normalmente implementato con la ricorsione della coda e rimarrà bloccato in un ciclo infinito, se non lo terminerà in anticipo.


3
È diverso ad Haskell a causa della pigrizia.
Lifu Huang
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.