¿Reducir, doblar o escanear (izquierda / derecha)?

187

¿Cuándo debo usar reduceLeft, reduceRight, foldLeft, foldRight, scanLefto scanRight?

Quiero una intuición / visión general de sus diferencias, posiblemente con algunos ejemplos simples.

Marc Grue
fuente
Te recomiendo que veas stackoverflow.com/questions/25158780/…
samthebest el
1
Gracias por la anotación. Está un poco por encima de mi conocimiento técnico :) ¿Hay algo en mi respuesta que crees que debería aclararse / cambiarse?
Marc Grue
No, solo señala un poco de historia y la relevancia para MPP.
samthebest
Bueno, estrictamente hablando, la distinción entre reducey foldNO es la existencia de un valor inicial, sino que es consecuencia de una razón matemática subyacente más profunda.
samthebest

Respuestas:

370

En general, las 6 funciones de plegado aplican un operador binario a cada elemento de una colección. El resultado de cada paso se pasa al siguiente paso (como entrada a uno de los dos argumentos del operador binario). De esta manera podemos acumular un resultado.

reduceLefty reduceRightacumular un solo resultado.

foldLefty foldRightacumular un solo resultado utilizando un valor inicial.

scanLefty scanRightacumular una colección de resultados acumulativos intermedios utilizando un valor inicial.

Acumular

De izquierda a derecha ...

Con una colección de elementos abcy un operador binario, addpodemos explorar lo que hacen las diferentes funciones de plegado al avanzar desde el elemento IZQUIERDO de la colección (de A a 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


Desde la DERECHA y hacia atrás ...

Si comenzamos con el elemento DERECHO y retrocedemos (de C a A), notaremos que ahora el segundo argumento para nuestro operador binario acumula el resultado (el operador es el mismo, solo cambiamos los nombres de los argumentos para aclarar sus roles ):

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)

.

Desacumular

De izquierda a derecha ...

Si, en cambio, dejáramos de acumular algún resultado restando a partir del elemento IZQUIERDO de una colección, acumularíamos el resultado a través del primer argumento resde nuestro operador 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)


Desde la DERECHA y hacia atrás ...

¡Pero busque las variaciones de xRight ahora! Recuerde que el valor (des) acumulado en las variaciones xRight se pasa al segundo parámetro resde nuestro operador 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) 

¡La última Lista (-2, 3, -1, 4, 0) tal vez no sea lo que intuitivamente esperaría!

Como puede ver, puede verificar qué está haciendo su foldX simplemente ejecutando un scanX en su lugar y depurar el resultado acumulado en cada paso.

Línea de fondo

  • Acumula un resultado con reduceLefto reduceRight.
  • Acumule un resultado con foldLefto foldRightsi tiene un valor inicial.
  • Acumula una colección de resultados intermedios con scanLefto scanRight.

  • Utilice una variación xLeft si quiere ir hacia delante a través de la colección.

  • Utilice una variación de xRight si desea retroceder en la colección.
Marc Grue
fuente
14
Si no me equivoco, la versión de la izquierda puede usar la optimización de llamadas de cola, lo que significa que es mucho más eficiente.
Trylks el
3
@Marc, me gustan los ejemplos con letras, dejó las cosas muy claras
Muhammad Farag
@ Trylks foldRight también se puede implementar con tailrec
Timothy Kim
@TimothyKim puede, con implementaciones no sencillas optimizadas para hacerlo. Por ejemplo, en el caso particular de las listas Scala , esa forma consiste en revertir el Listpara luego aplicar foldLeft. Otras colecciones pueden implementar diferentes estrategias. En general, si foldLefty foldRightpuede usarse indistintamente (propiedad asociativa del operador aplicado), entonces foldLeftes más eficiente y preferible.
Trylks
9

Normalmente, el método REDUCE, FOLD, SCAN funciona acumulando datos en IZQUIERDA y sigue cambiando la variable DERECHA. La principal diferencia entre ellos es REDUCIR, PLEGAR es: -

Fold siempre comenzará con un seedvalor, es decir, un valor inicial definido por el usuario. Reducir arrojará una excepción si la colección está vacía donde el pliegue devuelve el valor inicial. Siempre dará como resultado un único valor.

El escaneo se usa para el orden de procesamiento de artículos desde el lado izquierdo o derecho, luego podemos hacer uso del resultado anterior en el cálculo posterior. Eso significa que podemos escanear artículos. Siempre resultará una colección.

  • El método LEFT_REDUCE funciona de manera similar al método REDUCE.
  • RIGHT_REDUCE es opuesto a reducirLeft one, es decir, acumula valores en RIGHT y sigue cambiando la variable izquierda.

  • reduceLeftOption y reduceRightOption son similares a left_reduce y right_reduce, la única diferencia es que devuelven resultados en el objeto OPTION.

Una parte de la salida para el código mencionado a continuación sería: -

usando la scanoperación sobre una lista de números (usando el seedvalor 0)List(-2,-1,0,1,2)

  • {0, -2} => - 2 {-2, -1} => - 3 {-3,0} => - 3 {-3,1} => - 2 {-2,2} => 0 Lista de escaneo (0, -2, -3, -3, -2, 0)

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

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

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

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

utilizando reduce, foldlas operaciones a través de una lista de cadenasList("A","B","C","D","E")

  • {A, B} => AB {AB, C} => ABC {ABC, D} => ABCD {ABCD, E} => ABCDE reducir (a + b) ABCDE
  • {A, B} => AB {AB, C} => ABC {ABC, D} => ABCD {ABCD, E} => ABCDE reducir Izquierda (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 reduce Derecho (a + b) ABCDE
  • {D, E} => ED {C, ED} => EDC {B, EDC} => EDCB {A, EDCB} => EDCBA reduceRight (b + a) EDCBA

Código:

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}))
}
Puneeth Reddy V
fuente
9
Esta publicación es apenas legible. Acorte las oraciones, use palabras clave reales (por ejemplo, reduceLeft en lugar de LEFT_REDUCE) Use flechas matemáticas reales, etiquetas de código cuando trabaje con el código. Prefiere ejemplos de entrada / salida en lugar de explicar todo. Los cálculos intermedios dificultan la lectura.
Mikaël Mayer
4

Para la colección x con elementos x0, x1, x2, x3 y una función arbitraria f tiene lo siguiente:

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

En conclusión

  • scanes como foldpero también emite todos los valores intermedios
  • reduce no necesita un valor inicial que a veces es un poco más difícil de encontrar
  • fold necesita un valor inicial que sea un poco más difícil de encontrar:
    • 0 para sumas
    • 1 para productos
    • primer elemento para min (algunos podrían sugerir Integer.MAX_VALUE)
  • No estoy 100% seguro, pero parece que hay implementaciones equivalentes:
    • 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
raisercostin
fuente