Il modo in cui fold
differiscono 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 f
e 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
f
al 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 f
viene 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 f
usa 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 foldl
tecnicamente è ricorsivo alla coda, poiché l'intera espressione del risultato è costruita prima di valutare qualsiasi cosa, foldl
può causare un overflow dello stack!
D'altra parte, considera foldr
. È anche pigro, ma poiché viene eseguito in avanti , ogni applicazione di f
viene 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 foldr
volte funziona su elenchi infiniti quando foldl
non 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, foldr
con 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 foldr
possa fare tutto il foldl
possibile, 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 .
foldr
è meglio chefoldl
in Haskell , mentre è vero il contrario a Erlang (che ho imparato prima di Haskell ). Poiché Erlang non è pigro e le funzioni non sono curry , cosìfoldl
in Erlang si comporta comefoldl'
sopra. Questa è un'ottima risposta! Buon lavoro e grazie!