Implicazioni di foldr vs. foldl (o foldl ')


155

Innanzitutto, Real World Haskell , che sto leggendo, dice di non usare mai foldle invece di usare foldl'. Quindi mi fido.

Ma io sono confusa su quando utilizzare foldrvs. foldl'. Anche se riesco a vedere la struttura di come funzionano in modo diverso davanti a me, sono troppo stupido per capire quando "che è meglio". Immagino mi sembri che non dovrebbe davvero importare quale sia usato, dato che entrambi producono la stessa risposta (no?). In effetti, la mia precedente esperienza con questo costrutto è quella di Ruby injecte Clojure reduce, che non sembrano avere versioni "sinistra" e "destra". (Domanda a margine: quale versione usano?)

Qualsiasi intuizione che possa aiutare un tipo sfidato intelligente come me sarebbe molto apprezzata!

Risposte:


174

La ricorsione per foldr f x ysdove ys = [y1,y2,...,yk]appare

f y1 (f y2 (... (f yk x) ...))

mentre la ricorsione per foldl f x yssembra

f (... (f (f x y1) y2) ...) yk

Una differenza importante qui è che se il risultato di f x ypuò essere calcolato usando solo il valore di x, allora foldrnon è necessario esaminare l'intero elenco. Per esempio

foldr (&&) False (repeat False)

ritorna Falsementre

foldl (&&) False (repeat False)

non termina mai. (Nota: repeat Falsecrea un elenco infinito in cui si trova ogni elementoFalse .)

D'altra parte, la foldl'coda è ricorsiva e severa. Se sai che dovrai attraversare l'intero elenco, non importa cosa (ad es. Sommare i numeri in un elenco), allora foldl'è più efficiente nello spazio (e probabilmente nel tempo) di foldr.


2
In foldr viene valutato come f y1 thunk, quindi restituisce False, tuttavia in foldl, f non può conoscere nessuno dei suoi parametri. In Haskell, indipendentemente dal fatto che si tratti di ricorsione della coda o meno, entrambi possono causare overflow dei thunk, ovvero troppo grande. foldl 'può ridurre immediatamente il thunk lungo l'esecuzione.
Sawyer,

25
Per evitare confusione, notare che le parentesi non mostrano l'effettivo ordine di valutazione. Poiché Haskell è pigro, le espressioni più esterne verranno valutate per prime.
Lii,

Rispondi alla risposta. Vorrei aggiungere che se vuoi una piega che può fermarsi a metà di una lista, devi usare foldr; a meno che non mi sbagli, le pieghe a sinistra non possono essere fermate. (Lo accenni quando dici "se sai ... attraverserai l'intera lista"). Inoltre, il refuso "usando solo il valore" dovrebbe essere cambiato in "usando solo il valore". Vale a dire rimuovere la parola "on". (Stackoverflow non mi consente di inviare una modifica di 2 caratteri!).
Lqueryvg,

@Lqueryvg due modi per fermare le pieghe a sinistra: 1. codificalo con una piega a destra (vedi fodlWhile); 2. convertilo in una scansione a sinistra ( scanl) e fermalo con last . takeWhile po simile. Uh e 3. use mapAccumL. :)
Will Ness,

1
@Desty perché produce una nuova parte del suo risultato complessivo su ogni passaggio - diversamente foldl, che raccoglie il suo risultato complessivo e lo produce solo dopo che tutto il lavoro è finito e non ci sono più passaggi da eseguire. Quindi foldl (flip (:)) [] [1..3] == [3,2,1], per esempio , quindi scanl (flip(:)) [] [1..] = [[],[1],[2,1],[3,2,1],...]... IOW, foldl f z xs = last (scanl f z xs)e gli elenchi infiniti non hanno l'ultimo elemento (che, nell'esempio sopra, sarebbe esso stesso un elenco infinito, da INF fino a 1).
Will Ness,

57

foldr Somiglia a questo:

Visualizzazione con piega a destra

foldl Somiglia a questo:

Visualizzazione a sinistra

Contesto: piega sul wiki di Haskell


33

La loro semantica differisce quindi non puoi semplicemente scambiare foldlefoldr . L'uno piega gli elementi da sinistra, l'altro da destra. In questo modo, l'operatore viene applicato in un ordine diverso. Questo è importante per tutte le operazioni non associative, come la sottrazione.

Haskell.org ha un interessante articolo sull'argomento.


La loro semantica differisce solo in modo banale, in pratica insignificante: l'ordine degli argomenti della funzione usata. Per quanto riguarda l'interfaccia, continuano a essere scambiati. La vera differenza è, a quanto pare, solo l'ottimizzazione / implementazione.
Evi1M4chine

2
@ Evi1M4chine Nessuna differenza è banale. Al contrario, sono sostanziali (e, sì, significativi nella pratica). In effetti, se dovessi scrivere questa risposta oggi, enfatizzerei ancora di più questa differenza.
Konrad Rudolph,

23

In breve, foldrè meglio quando la funzione accumulatore è pigra sul suo secondo argomento. Maggiori informazioni su Stack Overflow di Haskell wiki (gioco di parole).


18

Il motivo foldl'è preferitofoldl 99% di tutti gli usi è che può essere eseguita in uno spazio costante per la maggior parte degli usi.

Prendi la funzione sum = foldl['] (+) 0. Quando foldl'viene utilizzata, la somma viene immediatamente calcolata, quindi l'applicazione suma un elenco infinito verrà eseguita per sempre, e molto probabilmente nello spazio costante (se stai usando cose come Ints, Doubles, Floats. IntegerS userai più dello spazio costante se il il numero diventa maggiore dimaxBound :: Int ).

Con foldl, viene creato un thunk (come una ricetta su come ottenere la risposta, che può essere valutata in seguito, anziché memorizzare la risposta). Questi thunk possono occupare molto spazio e, in questo caso, è molto meglio valutare l'espressione piuttosto che archiviare il thunk (portando a un overflow dello stack ... e portandoti a ... oh, non importa)

Spero che aiuti.


1
La grande eccezione è se la funzione passata foldlnon fa altro che applicare i costruttori a uno o più dei suoi argomenti.
dfeuer

Esiste un modello generale per quando foldlè effettivamente la scelta migliore? (Come le liste infinite quando foldrè la scelta sbagliata, per quanto riguarda l'ottimizzazione.?)
Evi1M4chine

@ Evi1M4chine non sono sicuro di cosa intendi per foldressere la scelta sbagliata per infiniti elenchi. In effetti, non dovresti usare foldlo foldl'per infiniti elenchi. Guarda la wiki di Haskell su overflow dello stack
KevinOrr,

14

A proposito, Ruby injecte Clojure reducesono foldl(o foldl1, a seconda della versione che usi). Di solito, quando esiste una sola forma in una lingua, si tratta di una piega a sinistra, tra cui Python reduce, Perl List::Util::reduce, C ++ accumulate, C # Aggregate, Smalltalk inject:into:, PHP array_reduce, Mathematica Fold, ecc. Le reduceimpostazioni predefinite comuni di Lisp a piega a sinistra, ma c'è un'opzione per la piega a destra.


2
Questo commento è utile ma apprezzerei le fonti.
titaniumdecoy

Il Lisp comune reducenon è pigro, quindi è foldl'e molte delle considerazioni qui non si applicano.
MicroVirus

Penso che intendi foldl', dato che sono lingue rigorose, no? Altrimenti, ciò non significa che tutte quelle versioni causeranno overflow dello stack come foldlfa?
Evi1M4chine

6

Come sottolinea Konrad , la loro semantica è diversa. Non hanno nemmeno lo stesso tipo:

ghci> :t foldr
foldr :: (a -> b -> b) -> b -> [a] -> b
ghci> :t foldl
foldl :: (a -> b -> a) -> a -> [b] -> a
ghci> 

Ad esempio, l'operatore di aggiunta elenco (++) può essere implementato con foldras

(++) = flip (foldr (:))

mentre

(++) = flip (foldl (:))

ti darà un errore di tipo.


Il loro tipo è lo stesso, appena cambiato, il che è irrilevante. La loro interfaccia e i risultati sono gli stessi, ad eccezione del brutto problema P / NP (leggi: elenchi infiniti). ;) L'ottimizzazione dovuta all'implementazione è l'unica differenza nella pratica, per quanto ne so.
Evi1M4chine

@ Evi1M4chine Questo non è corretto, guarda questo esempio: foldl subtract 0 [1, 2, 3, 4]valuta -10, mentre foldr subtract 0 [1, 2, 3, 4]valuta -2. foldlè in realtà 0 - 1 - 2 - 3 - 4mentre lo foldrè 4 - 3 - 2 - 1 - 0.
krapht

@krapht, foldr (-) 0 [1, 2, 3, 4]è -2ed foldl (-) 0 [1, 2, 3, 4]è -10. D'altra parte, subtractè al contrario di ciò che ci si potrebbe aspettare ( subtract 10 14è 4), così foldr subtract 0 [1, 2, 3, 4]è -10ed foldl subtract 0 [1, 2, 3, 4]è 2(positivo).
pianoJames,
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.