Reduce, fold or scan (Left/Right)?

前端 未结 3 689
孤城傲影
孤城傲影 2020-11-28 00:35

When should I use reduceLeft, reduceRight, foldLeft, foldRight, scanLeft or scanRight?

3条回答
  •  感动是毒
    2020-11-28 00:47

    In general, all 6 fold functions apply a binary operator to each element of a collection. The result of each step is passed on to the next step (as input to one of the binary operator's two arguments). This way we can cumulate a result.

    reduceLeft and reduceRight cumulate a single result.

    foldLeft and foldRight cumulate a single result using a start value.

    scanLeft and scanRight cumulate a collection of intermediate cumulative results using a start value.

    Accumulate

    From LEFT and forwards...

    With a collection of elements abc and a binary operator add we can explore what the different fold functions do when going forwards from the LEFT element of the collection (from A to 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
    


    From RIGHT and backwards...

    If we start with the RIGHT element and go backwards (from C to A) we'll notice that now the second argument to our binary operator accumulates the result (the operator is the same, we just switched the argument names to make their roles clear):

    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-cumulate

    From LEFT and forwards...

    If instead we were to de-cumulate some result by subtraction starting from the LEFT element of a collection, we would cumulate the result through the first argument res of our binary operator 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)
    


    From RIGHT and backwards...

    But look out for the xRight variations now! Remember that the (de-)cumulated value in the xRight variations is passed to the second parameter res of our binary operator 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) 
    

    The last List(-2, 3, -1, 4, 0) is maybe not what you would intuitively expect!

    As you see, you can check what your foldX is doing by simply running a scanX instead and debug the cumulated result at each step.

    Bottom line

    • Cumulate a result with reduceLeft or reduceRight.
    • Cumulate a result with foldLeft or foldRight if you have a start value.
    • Cumulate a collection of intermediate results with scanLeft or scanRight.

    • Use a xLeft variation if you want to go forwards through the collection.

    • Use a xRight variation if you want to go backwards through the collection.

提交回复
热议问题