Ridurre, piegare o scansionare (sinistra / destra)?


187

Quando devo utilizzare reduceLeft, reduceRight, foldLeft, foldRight, scanLefto scanRight?

Voglio un'intuizione / panoramica delle loro differenze, possibilmente con alcuni semplici esempi.



1
Grazie per il puntatore. È un po 'al di sopra delle mie conoscenze tecniche :) C'è qualcosa nella mia risposta che ritieni debba essere chiarito / modificato?
Marc Grue,

No, solo sottolineando un po 'di storia e la rilevanza per MPP.
Samthebest,

Bene, a rigor di termini la distinzione tra reducee foldNON è l'esistenza di un valore iniziale - piuttosto che è una conseguenza di una ragione matematica sottostante più profonda.
Samthebest,

Risposte:


370

In generale, tutte e 6 le funzioni di piega applicano un operatore binario a ciascun elemento di una raccolta. Il risultato di ogni passaggio viene passato al passaggio successivo (come input per uno dei due argomenti dell'operatore binario). In questo modo possiamo accumulare un risultato.

reduceLefte reduceRightaccumulare un singolo risultato.

foldLefte foldRightaccumulare un singolo risultato usando un valore iniziale.

scanLefte scanRightaccumulare una raccolta di risultati cumulativi intermedi utilizzando un valore iniziale.

Accumulare

Da SINISTRA e in avanti ...

Con una raccolta di elementi abce un operatore binario addpossiamo esplorare cosa fanno le diverse funzioni di piega quando si procede in avanti dall'elemento SINISTRA della raccolta (dalla A alla C):

val abc = List("A", "B", "C")

def add(res: String, x: String) = { 
  println(s"op: $res + $x = ${res + x}")
  res + x
}

abc.reduceLeft(add)
// op: A + B = AB
// op: AB + C = ABC    // accumulates value AB in *first* operator arg `res`
// res: String = ABC

abc.foldLeft("z")(add) // with start value "z"
// op: z + A = zA      // initial extra operation
// op: zA + B = zAB
// op: zAB + C = zABC
// res: String = zABC

abc.scanLeft("z")(add)
// op: z + A = zA      // same operations as foldLeft above...
// op: zA + B = zAB
// op: zAB + C = zABC
// res: List[String] = List(z, zA, zAB, zABC) // maps intermediate results


Da DESTRA e indietro ...

Se iniziamo con l'elemento DESTRA e torniamo indietro (da C ad A) noteremo che ora il secondo argomento per il nostro operatore binario accumula il risultato (l'operatore è lo stesso, abbiamo semplicemente cambiato i nomi degli argomenti per chiarire i loro ruoli ):

def add(x: String, res: String) = {
  println(s"op: $x + $res = ${x + res}")
  x + res
}

abc.reduceRight(add)
// op: B + C = BC
// op: A + BC = ABC  // accumulates value BC in *second* operator arg `res`
// res: String = ABC

abc.foldRight("z")(add)
// op: C + z = Cz
// op: B + Cz = BCz
// op: A + BCz = ABCz
// res: String = ABCz

abc.scanRight("z")(add)
// op: C + z = Cz
// op: B + Cz = BCz
// op: A + BCz = ABCz
// res: List[String] = List(ABCz, BCz, Cz, z)

.

De-cumulare

Da SINISTRA e in avanti ...

Se invece dovessimo cumulare alcuni risultati per sottrazione a partire dall'elemento LEFT di una raccolta, accumuleremmo il risultato attraverso il primo argomento resdel nostro operatore binario minus:

val xs = List(1, 2, 3, 4)

def minus(res: Int, x: Int) = {
  println(s"op: $res - $x = ${res - x}")
  res - x
}

xs.reduceLeft(minus)
// op: 1 - 2 = -1
// op: -1 - 3 = -4  // de-cumulates value -1 in *first* operator arg `res`
// op: -4 - 4 = -8
// res: Int = -8

xs.foldLeft(0)(minus)
// op: 0 - 1 = -1
// op: -1 - 2 = -3
// op: -3 - 3 = -6
// op: -6 - 4 = -10
// res: Int = -10

xs.scanLeft(0)(minus)
// op: 0 - 1 = -1
// op: -1 - 2 = -3
// op: -3 - 3 = -6
// op: -6 - 4 = -10
// res: List[Int] = List(0, -1, -3, -6, -10)


Da DESTRA e indietro ...

Ma cerca subito le varianti di xRight! Ricorda che il valore (de) cumulato nelle variazioni di xRight viene passato al secondo parametro resdel nostro operatore binario minus:

def minus(x: Int, res: Int) = {
  println(s"op: $x - $res = ${x - res}")
  x - res
}

xs.reduceRight(minus)
// op: 3 - 4 = -1
// op: 2 - -1 = 3  // de-cumulates value -1 in *second* operator arg `res`
// op: 1 - 3 = -2
// res: Int = -2

xs.foldRight(0)(minus)
// op: 4 - 0 = 4
// op: 3 - 4 = -1
// op: 2 - -1 = 3
// op: 1 - 3 = -2
// res: Int = -2

xs.scanRight(0)(minus)
// op: 4 - 0 = 4
// op: 3 - 4 = -1
// op: 2 - -1 = 3
// op: 1 - 3 = -2
// res: List[Int] = List(-2, 3, -1, 4, 0) 

L'ultimo elenco (-2, 3, -1, 4, 0) non è forse quello che ti aspetteresti intuitivamente!

Come vedi, puoi controllare cosa sta facendo il tuo foldX semplicemente eseguendo invece scanX ed eseguendo il debug del risultato cumulativo ad ogni passaggio.

Linea di fondo

  • Cumulare un risultato con reduceLefto reduceRight.
  • Cumula un risultato con foldLefto foldRightse hai un valore iniziale.
  • Cumulare una raccolta di risultati intermedi con scanLefto scanRight.

  • Utilizzare una variante xLeft se si desidera andare avanti nella raccolta.

  • Utilizzare una variante xRight se si desidera andare indietro nella raccolta.

14
Se non sbaglio, la versione di sinistra può utilizzare l'ottimizzazione delle chiamate di coda, il che significa che è molto più efficiente.
Trylks,

3
@Marc, mi piacciono gli esempi con le lettere, ha reso le cose molto chiare
Muhammad Farag,

@ Trylks foldRight può anche essere implementato con tailrec
Timothy Kim

@TimothyKim può, con implementazioni non semplici ottimizzate per farlo. Ad esempio, nel caso particolare degli elenchi Scala , in questo modo consiste nell'invertire l'applicazione Listper poi applicare foldLeft. Altre collezioni possono implementare strategie diverse. In generale, se foldLefte foldRightpuò essere usato in modo intercambiabile (proprietà associativa dell'operatore applicato), allora foldLeftè più efficiente e preferibile.
Trylks,

9

Normalmente il metodo REDUCE, FOLD, SCAN funziona accumulando dati su SINISTRA e continuando a cambiare la variabile DESTRA. La principale differenza tra loro è REDUCE, FOLD è: -

Fold inizierà sempre con un seedvalore, ovvero un valore iniziale definito dall'utente. Riduci genererà un'eccezione se la raccolta è vuota, in quanto la piega restituisce il valore del seme. Risulterà sempre un singolo valore.

La scansione viene utilizzata per un certo ordine di elaborazione degli articoli dal lato sinistro o destro, quindi possiamo utilizzare il risultato precedente nel calcolo successivo. Ciò significa che possiamo scansionare oggetti. Risulterà sempre una raccolta.

  • Il metodo LEFT_REDUCE funziona in modo simile al metodo REDUCE.
  • RIGHT_REDUCE è l'opposto di ridurre Sinistra uno, ovvero accumula valori in DESTRA e continua a cambiare la variabile sinistra.

  • reduceLeftOption e reduceRightOption sono simili a left_reduce e right_reduce l'unica differenza è che restituiscono risultati nell'oggetto OPTION.

Una parte dell'output per il codice di seguito indicato sarebbe: -

utilizzando l' scanoperazione su un elenco di numeri (utilizzando il seedvalore 0)List(-2,-1,0,1,2)

  • {0, -2} => - 2 {-2, -1} => - 3 {-3,0} => - 3 {-3,1} => - 2 {-2,2} => 0 Elenco di scansione (0, -2, -3, -3, -2, 0)

  • {0, -2} => - 2 {-2, -1} => - 3 {-3,0} => - 3 {-3,1} => - 2 {-2,2} => 0 scanLeft (a + b) Elenco (0, -2, -3, -3, -2, 0)

  • {0, -2} => - 2 {-2, -1} => - 3 {-3,0} => - 3 {-3,1} => - 2 {-2,2} => 0 scanLeft (b + a) Elenco (0, -2, -3, -3, -2, 0)

  • {2,0} => 2 {1,2} => 3 {0,3} => 3 {-1,3} => 2 {-2,2} => 0 scanRight (a + b) Elenco ( 0, 2, 3, 3, 2, 0)

  • {2,0} => 2 {1,2} => 3 {0,3} => 3 {-1,3} => 2 {-2,2} => 0 scanRight (b + a) List ( 0, 2, 3, 3, 2, 0)

utilizzando reduce, foldoperazioni su un elenco di stringheList("A","B","C","D","E")

  • {A, B} => AB {AB, C} => ABC {ABC, D} => ABCD {ABCD, E} => ABCDE ridurre (a + b) ABCDE
  • {A, B} => AB {AB, C} => ABC {ABC, D} => ABCD {ABCD, E} => ABCDE ridurre Sinistra (a + b) ABCDE
  • {A, B} => BA {BA, C} => CBA {CBA, D} => DCBA {DCBA, E} => EDCBA reduceLeft (b + a) EDCB
  • {D, E} => DE {C, DE} => CDE {B, CDE} => BCDE {A, BCDE} => ABCDE reduceRight (a + b) ABCDE
  • {D, E} => ED {C, ED} => EDC {B, EDC} => EDCB {A, EDCB} => EDCBA reduceRight (b + a) EDCBA

Codice :

object ScanFoldReduce extends App {

    val list = List("A","B","C","D","E")
            println("reduce (a+b) "+list.reduce((a,b)=>{
                print("{"+a+","+b+"}=>"+ (a+b)+"  ")
                a+b
            }))

            println("reduceLeft (a+b) "+list.reduceLeft((a,b)=>{
                print("{"+a+","+b+"}=>"+ (a+b)+"  ")
                a+b
            }))

            println("reduceLeft (b+a) "+list.reduceLeft((a,b)=>{
                print("{"+a+","+b+"}=>"+ (b+a)+"  " )
                b+a
            }))

            println("reduceRight (a+b) "+list.reduceRight((a,b)=>{
                print("{"+a+","+b+"}=>"+ (a+b)+"  " )
                a+b
            }))

            println("reduceRight (b+a) "+list.reduceRight((a,b)=>{
                print("{"+a+","+b+"}=>"+ (b+a)+"  ")
                b+a
            }))

            println("scan            "+list.scan("[")((a,b)=>{
                print("{"+a+","+b+"}=>"+ (a+b)+"  " )
                a+b
            }))
            println("scanLeft (a+b)  "+list.scanLeft("[")((a,b)=>{
                print("{"+a+","+b+"}=>"+ (a+b)+"  " )
                a+b
            }))
            println("scanLeft (b+a)  "+list.scanLeft("[")((a,b)=>{
                print("{"+a+","+b+"}=>"+ (b+a)+"  " )
                b+a
            }))
            println("scanRight (a+b) "+list.scanRight("[")((a,b)=>{
                print("{"+a+","+b+"}=>"+ (a+b)+"  " )
                a+b
            }))
            println("scanRight (b+a) "+list.scanRight("[")((a,b)=>{
                print("{"+a+","+b+"}=>"+ (b+a)+"  " )
                b+a
            }))
//Using numbers
     val list1 = List(-2,-1,0,1,2)

            println("reduce (a+b) "+list1.reduce((a,b)=>{
                print("{"+a+","+b+"}=>"+ (a+b)+"  ")
                a+b
            }))

            println("reduceLeft (a+b) "+list1.reduceLeft((a,b)=>{
                print("{"+a+","+b+"}=>"+ (a+b)+"  ")
                a+b
            }))

            println("reduceLeft (b+a) "+list1.reduceLeft((a,b)=>{
                print("{"+a+","+b+"}=>"+ (b+a)+"  " )
                b+a
            }))

            println("      reduceRight (a+b) "+list1.reduceRight((a,b)=>{
                print("{"+a+","+b+"}=>"+ (a+b)+"  " )
                a+b
            }))

            println("      reduceRight (b+a) "+list1.reduceRight((a,b)=>{
                print("{"+a+","+b+"}=>"+ (b+a)+"  ")
                b+a
            }))

            println("scan            "+list1.scan(0)((a,b)=>{
                print("{"+a+","+b+"}=>"+ (a+b)+"  " )
                a+b
            }))

            println("scanLeft (a+b)  "+list1.scanLeft(0)((a,b)=>{
                print("{"+a+","+b+"}=>"+ (a+b)+"  " )
                a+b
            }))

            println("scanLeft (b+a)  "+list1.scanLeft(0)((a,b)=>{
                print("{"+a+","+b+"}=>"+ (b+a)+"  " )
                b+a
            }))

            println("scanRight (a+b)         "+list1.scanRight(0)((a,b)=>{
                print("{"+a+","+b+"}=>"+ (a+b)+"  " )
                a+b}))

            println("scanRight (b+a)         "+list1.scanRight(0)((a,b)=>{
                print("{"+a+","+b+"}=>"+ (a+b)+"  " )
                b+a}))
}

9
Questo post è a malapena leggibile. Riduci le frasi, utilizza parole chiave reali (ad esempio, riduci Sinistra anziché LEFT_REDUCE). Usa frecce matematiche reali, tag di codice quando hai a che fare con il codice. Preferisci esempi di input / output piuttosto che spiegare tutto. I calcoli intermedi rendono difficile la lettura.
Mikaël Mayer,

4

Per la raccolta x con elementi x0, x1, x2, x3 e una funzione arbitraria f si dispone di quanto segue:

1. x.reduceLeft    (f) is f(f(f(x0,x1),x2),x3) - notice 3 function calls
2. x.reduceRight   (f) is f(f(f(x3,x2),x1),x0) - notice 3 function calls
3. x.foldLeft (init,f) is f(f(f(f(init,x0),x1),x2),x3) - notice 4 function calls
4. x.foldRight(init,f) is f(f(f(f(init,x3),x2),x1),x0) - notice 4 function calls
5. x.scanLeft (init,f) is f(init,x0)=g0
                          f(f(init,x0),x1) = f(g0,x1) = g1
                          f(f(f(init,x0),x1),x2) = f(g1,x2) = g2
                          f(f(f(f(init,x0),x1),x2),x3) = f(g2,x3) = g3
                          - notice 4 function calls but also 4 emitted values
                          - last element is identical with foldLeft
6. x.scanRight (init,f) is f(init,x3)=h0
                          f(f(init,x3),x2) = f(h0,x2) = h1
                          f(f(f(init,x3),x2),x1) = f(h1,x1) = h2
                          f(f(f(f(init,x3),x2),x1),x0) = f(h2,x0) = h3
                          - notice 4 function calls but also 4 emitted values
                          - last element is identical with foldRight

In conclusione

  • scanè come foldma emette anche tutti i valori intermedi
  • reduce non ha bisogno di un valore iniziale che a volte è un po 'più difficile da trovare
  • fold ha bisogno di un valore iniziale che sia un po 'più difficile da trovare:
    • 0 per somme
    • 1 per i prodotti
    • primo elemento per min (alcuni potrebbero suggerire Integer.MAX_VALUE)
  • non sicuro al 100% ma sembra che ci siano queste implementazioni equivalenti:
    • x.reduceLeft(f) === x.drop(1).foldLeft(x.head,f)
    • x.foldRight(init,f) === x.reverse.foldLeft(init,f)
    • x.foldLeft(init,f) === x.scanLeft(init,f).last
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.