Come fai a sapere quando usare piega a sinistra e quando usare piega a destra?


99

Sono consapevole che piega a sinistra produce alberi inclinati a sinistra e piega a destra produce alberi inclinati a destra, ma quando raggiungo una piega, a volte mi ritrovo impantanata in pensieri che inducono mal di testa cercando di determinare quale tipo di piega è appropriato. Di solito finisco per risolvere l'intero problema e passare attraverso l'implementazione della funzione di piegatura in quanto si applica al mio problema.

Quindi quello che voglio sapere è:

  • Quali sono alcune regole pratiche per determinare se piegare a sinistra o piegare a destra?
  • Come posso decidere rapidamente quale tipo di piega utilizzare visto il problema che sto affrontando?

C'è un esempio in Scala by Example (PDF) di utilizzare una piega per scrivere una funzione chiamata flatten che concatena un elenco di elenchi di elementi in un unico elenco. In tal caso, una piega a destra è la scelta corretta (visto il modo in cui le liste sono concatenate), ma ho dovuto pensarci un po 'per arrivare a quella conclusione.

Poiché il ripiegamento è un'azione così comune nella programmazione (funzionale), mi piacerebbe essere in grado di prendere questo tipo di decisioni in modo rapido e sicuro. Quindi ... qualche consiglio?



1
Questo Q è più generale di quello che riguardava specificamente Haskell. La pigrizia fa una grande differenza nella risposta alla domanda.
Chris Conway

Oh. Strano, in qualche modo pensavo di aver visto un tag Haskell su questa domanda, ma immagino di no ...
effimero

Risposte:


105

Puoi trasferire una piega in una notazione operatore infisso (scrivendo in mezzo):

Questo esempio piega utilizzando la funzione accumulatore x

fold x [A, B, C, D]

quindi è uguale

A x B x C x D

Ora non ti resta che ragionare sull'associatività del tuo operatore (mettendo parentesi!).

Se hai un operatore associativo a sinistra , imposterai le parentesi in questo modo

((A x B) x C) x D

Qui, usi una piega a sinistra . Esempio (pseudocodice in stile haskell)

foldl (-) [1, 2, 3] == (1 - 2) - 3 == 1 - 2 - 3 // - is left-associative

Se il tuo operatore è associativo a destra ( piega a destra ), le parentesi sarebbero impostate in questo modo:

A x (B x (C x D))

Esempio: contro-operatore

foldr (:) [] [1, 2, 3] == 1 : (2 : (3 : [])) == 1 : 2 : 3 : [] == [1, 2, 3]

In generale, gli operatori aritmetici (la maggior parte degli operatori) sono associativi a sinistra, quindi foldlè più diffuso. Ma negli altri casi, la notazione infissa + parentesi è abbastanza utile.


6
Bene, ciò che hai descritto è effettivamente foldl1e foldr1in Haskell ( foldle foldrprende un valore iniziale esterno), e il "contro" di Haskell è chiamato (:)no (::), ma per il resto è corretto. Potresti aggiungere che Haskell fornisce inoltre un foldl'/ foldl1'che sono varianti rigorose di foldl/ foldl1, perché l'aritmetica pigra non è sempre desiderabile.
effimero

Mi spiace, pensavo di aver visto un tag "Haskell" su questa domanda, ma non è presente. Il mio commento non ha molto senso se non è Haskell ...
effimero

@ephemient L'hai visto. È "pseudocodice in stile haskell". :)
laughing_man

La migliore risposta che abbia mai visto relativa alla differenza tra le pieghe.
AleXoundOS

60

Olin Shivers li ha differenziati dicendo "foldl è l'iteratore di lista fondamentale" e "foldr è l'operatore di ricorsione di lista fondamentale". Se guardi come funziona Foldl:

((1 + 2) + 3) + 4

puoi vedere l'accumulatore (come in un'iterazione ricorsiva in coda) in costruzione. Al contrario, foldr procede:

1 + (2 + (3 + 4))

dove puoi vedere l'attraversamento al caso base 4 e costruire il risultato da lì.

Quindi pongo una regola pratica: se sembra un'iterazione di lista, una che sarebbe semplice da scrivere in forma ricorsiva di coda, foldl è la strada da percorrere.

Ma in realtà questo sarà probabilmente più evidente dall'associatività degli operatori che stai utilizzando. Se sono associativi a sinistra, usa foldl. Se sono associativi a destra, usa foldr.


29

Altri poster hanno dato buone risposte e non ripeterò ciò che hanno già detto. Dato che hai fornito un esempio di Scala nella tua domanda, fornirò un esempio specifico di Scala. Come già detto da Tricks , è foldRightnecessario preservare gli n-1stack frame, dov'è nla lunghezza del tuo elenco e questo può facilmente portare a un overflow dello stack - e nemmeno la ricorsione della coda potrebbe salvarti da questo.

A List(1,2,3).foldRight(0)(_ + _)si ridurrebbe a:

1 + List(2,3).foldRight(0)(_ + _)        // first stack frame
    2 + List(3).foldRight(0)(_ + _)      // second stack frame
        3 + 0                            // third stack frame 
// (I don't remember if the JVM allocates space 
// on the stack for the third frame as well)

mentre List(1,2,3).foldLeft(0)(_ + _)si ridurrebbe a:

(((0 + 1) + 2) + 3)

che può essere calcolato iterativamente, come fatto nell'implementazione diList .

In un linguaggio rigorosamente valutato come Scala, a foldRightpuò facilmente far saltare in aria la pila per elenchi di grandi dimensioni, mentre a foldLeftnon lo farà.

Esempio:

scala> List.range(1, 10000).foldLeft(0)(_ + _)
res1: Int = 49995000

scala> List.range(1, 10000).foldRight(0)(_ + _)
java.lang.StackOverflowError
        at scala.List.foldRight(List.scala:1081)
        at scala.List.foldRight(List.scala:1081)
        at scala.List.foldRight(List.scala:1081)
        at scala.List.foldRight(List.scala:1081)
        at scala.List.foldRight(List.scala:1081)
        at scala.List.foldRight(List.scala:1081)
        at scala.List.foldRight(List.scala:1081)
        at scala.List.foldRight(List.scala:1081)
        at scala.List.foldRig...

La mia regola pratica è quindi: per gli operatori che non hanno un'associatività specifica, usa sempre foldLeft, almeno in Scala. Altrimenti, segui altri consigli forniti nelle risposte;).


13
Questo era vero in precedenza, ma nelle attuali versioni di Scala, foldRight è stato modificato per applicare foldLeft su una copia invertita della lista. Ad esempio, in 2.10.3, github.com/scala/scala/blob/v2.10.3/src/library/scala/… . Sembra che questa modifica sia stata apportata all'inizio del 2013 - github.com/scala/scala/commit/… .
Dhruv Kapoor

4

Vale anche la pena notare (e mi rendo conto che questo sta affermando un po 'l'ovvio), nel caso di un operatore commutativo i due sono praticamente equivalenti. In questa situazione un foldl potrebbe essere la scelta migliore:

foldl: (((1 + 2) + 3) + 4)può calcolare ogni operazione e portare avanti il ​​valore accumulato

foldr: (1 + (2 + (3 + 4)))necessita di uno stack frame da aprire per 1 + ?e 2 + ?prima del calcolo 3 + 4, quindi deve tornare indietro ed eseguire il calcolo per ciascuno.

Non sono abbastanza esperto di linguaggi funzionali o ottimizzazioni del compilatore per dire se questo farà effettivamente la differenza, ma certamente sembra più pulito usare un foldl con operatori commutativi.


1
Gli stack frame extra faranno sicuramente la differenza per elenchi di grandi dimensioni. Se i tuoi stack frame superano la dimensione della cache del processore, i tuoi errori di cache influenzeranno le prestazioni. A meno che la lista non sia doppiamente collegata, è un po 'difficile rendere foldr una funzione ricorsiva di coda, quindi dovresti usare foldl a meno che non ci sia un motivo per non farlo.
A. Levy

4
La natura pigra di Haskell confonde questa analisi. Se la funzione che viene piegata non è rigorosa nel secondo parametro, foldrpuò essere molto più efficiente di foldle non richiederà alcun frame dello stack aggiuntivo.
effimero

2
Mi spiace, pensavo di aver visto un tag "Haskell" su questa domanda, ma non è presente. Il mio commento non ha davvero molto senso se non è Haskell ...
effimero
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.