Giảm, gấp hoặc quét (Trái / Phải)?


186

Khi nào tôi nên sử dụng reduceLeft, reduceRight, foldLeft, foldRight, scanLefthay scanRight?

Tôi muốn một trực giác / tổng quan về sự khác biệt của họ - có thể với một số ví dụ đơn giản.



1
Cảm ơn con trỏ. Đó là một chút trên kiến ​​thức kỹ thuật của tôi :) Có điều gì trong câu trả lời của tôi mà bạn nghĩ nên được làm rõ / thay đổi?
Marc Grue

Không, chỉ nêu ra một chút lịch sử và sự liên quan đến MPP.
samthebest

Vâng, nói đúng ra sự khác biệt giữa reducefoldKHÔNG phải là sự tồn tại của giá trị bắt đầu - đúng hơn đó là hậu quả của một lý do toán học sâu xa hơn.
samthebest

Câu trả lời:


370

Nói chung, tất cả 6 hàm gấp đều áp dụng toán tử nhị phân cho mỗi phần tử của tập hợp. Kết quả của mỗi bước được chuyển sang bước tiếp theo (làm đầu vào cho một trong hai đối số của toán tử nhị phân). Bằng cách này chúng ta có thể tích lũy một kết quả.

reduceLeftreduceRighttích lũy một kết quả duy nhất.

foldLeftfoldRighttích lũy một kết quả duy nhất bằng cách sử dụng giá trị bắt đầu.

scanLeftscanRighttích lũy một tập hợp các kết quả tích lũy trung gian bằng cách sử dụng giá trị bắt đầu.

Tích trữ

Từ TRÁI và chuyển tiếp ...

Với bộ sưu tập các phần tử abcvà toán tử nhị phân, addchúng ta có thể khám phá các hàm gấp khác nhau làm gì khi chuyển tiếp từ phần tử LEFT của bộ sưu tập (từ A đến 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


Từ QUYỀN và ngược ...

Nếu chúng ta bắt đầu với phần tử ĐÚNG và quay ngược (từ C sang A), chúng ta sẽ nhận thấy rằng bây giờ đối số thứ hai cho toán tử nhị phân của chúng ta tích lũy kết quả (toán tử giống nhau, chúng ta chỉ cần chuyển tên đối số để làm rõ vai trò của chúng ):

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)

.

Khử tích lũy

Từ TRÁI và chuyển tiếp ...

Nếu thay vào đó, chúng tôi sẽ tích lũy một số kết quả bằng phép trừ bắt đầu từ phần tử TRÁI của bộ sưu tập, chúng tôi sẽ tích lũy kết quả thông qua đối số đầu tiên rescủa toán tử nhị phân của chúng tôi 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)


Từ QUYỀN và ngược ...

Nhưng xem ra các biến thể xRight bây giờ! Hãy nhớ rằng giá trị tích lũy (khử) trong các biến thể xRight được truyền cho tham số thứ haires của toán tử nhị phân của chúng tôi 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) 

Danh sách cuối cùng (-2, 3, -1, 4, 0) có thể không phải là những gì bạn mong đợi bằng trực giác!

Như bạn thấy, bạn có thể kiểm tra xem FoldX của bạn đang làm gì bằng cách chạy scanX thay vào đó và gỡ lỗi kết quả tích lũy ở mỗi bước.

Dòng dưới cùng

  • Tích lũy một kết quả với reduceLefthoặc reduceRight.
  • Tích lũy một kết quả với foldLefthoặc foldRightnếu bạn có giá trị bắt đầu.
  • Tích lũy một tập hợp các kết quả trung gian với scanLefthoặc scanRight.

  • Sử dụng biến thể xLeft nếu bạn muốn chuyển tiếp qua bộ sưu tập.

  • Sử dụng biến thể xRight nếu bạn muốn quay ngược qua bộ sưu tập.

14
Nếu tôi không nhầm, phiên bản bên trái có thể sử dụng tối ưu hóa cuộc gọi đuôi, có nghĩa là nó hiệu quả hơn nhiều.
Trylks

3
@Marc, tôi thích các ví dụ với các chữ cái, nó làm cho mọi thứ rất rõ ràng
Muhammad Farag

@Trylks FoldRight cũng có thể được triển khai với tailrec
Timothy Kim

@TimothyKim có thể, với các triển khai không đơn giản được tối ưu hóa để làm như vậy. Ví dụ: Trong trường hợp cụ thể của danh sách Scala , cách đó bao gồm việc đảo ngược Listđể sau đó áp dụng foldLeft. Các bộ sưu tập khác có thể thực hiện các chiến lược khác nhau. Nói chung, nếu foldLeftfoldRightcó thể được sử dụng thay thế cho nhau (thuộc tính kết hợp của toán tử ứng dụng), thì foldLefthiệu quả hơn và tốt hơn.
Trylks

9

Thông thường phương pháp GIẢM, KHUÔN, QUÉT hoạt động bằng cách tích lũy dữ liệu trên TRÁI và tiếp tục thay đổi biến PHẢI. Sự khác biệt chính giữa chúng là GIẢM, KHUÔN là: -

Fold sẽ luôn bắt đầu với một seedgiá trị tức là giá trị bắt đầu do người dùng xác định. Giảm sẽ ném một ngoại lệ nếu bộ sưu tập trống trong khi nếp gấp trả lại giá trị hạt giống. Sẽ luôn luôn dẫn đến một giá trị duy nhất.

Quét được sử dụng cho một số thứ tự xử lý các mục từ bên trái hoặc bên phải, sau đó chúng ta có thể sử dụng kết quả trước đó trong tính toán tiếp theo. Điều đó có nghĩa là chúng ta có thể quét các mục. Sẽ luôn luôn kết quả một bộ sưu tập.

  • Phương pháp LEFT_REDUCE hoạt động tương tự như Phương pháp GIẢM.
  • RIGHT_REDUCE ngược lại với lessLeft one tức là nó tích lũy các giá trị trong RIGHT và tiếp tục thay đổi biến trái.

  • lessLeftOption và lessRightOption tương tự như left_reduce và right_reduce chỉ khác nhau là chúng trả về kết quả trong đối tượng OPTION.

Một phần đầu ra cho mã được đề cập dưới đây sẽ là: -

sử dụng scanthao tác trên một danh sách các số (sử dụng seedgiá trị 0)List(-2,-1,0,1,2)

  • {0, -2} => - 2 {-2, -1} => - 3 {-3,0} => - 3 {-3,1} => - 2 {-2,2} => 0 Danh sách quét (0, -2, -3, -3, -2, 0)

  • {0, -2} => - 2 {-2, -1} => - 3 {-3,0} => - 3 {-3,1} => - 2 {-2,2} => 0 danh sách quétLeft (a + b) (0, -2, -3, -3, -2, 0)

  • {0, -2} => - 2 {-2, -1} => - 3 {-3,0} => - 3 {-3,1} => - 2 {-2,2} => 0 danh sách quétLeft (b + a) (0, -2, -3, -3, -2, 0)

  • {2,0} => 2 {1,2} => 3 {0,3} => 3 {-1,3} => 2 {-2,2} => 0 scanRight (a + b) Danh sách ( 0, 2, 3, 3, 2, 0)

  • {2,0} => 2 {1,2} => 3 {0,3} => 3 {-1,3} => 2 {-2,2} => 0 scanRight (b + a) Danh sách ( 0, 2, 3, 3, 2, 0)

sử dụng reduce, foldthao tác trên một danh sách các ChuỗiList("A","B","C","D","E")

  • {A, B} => AB {AB, C} => ABC {ABC, D} => ABCD {ABCD, E} => ABCDE giảm (a + b) ABCDE
  • {A, B} => AB {AB, C} => ABC {ABC, D} => ABCD {ABCD, E} => ABCDE giảmLeft (a + b) ABCDE
  • {A, B} => BA {BA, C} => CBA {CBA, D} => DCBA {DCBA, E} => EDCBA giảmLeft (b + a) EDCB
  • {D, E} => DE {C, DE} => CDE {B, CDE} => BCDE {A, BCDE} => ABCDE giảmRight (a + b) ABCDE
  • {D, E} => ED {C, ED} => EDC {B, EDC} => EDCB {A, EDCB} => EDCBA giảmRight (b + a) EDCBA

Mã số:

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
Bài này hầu như không thể đọc được. Vui lòng rút ngắn câu, sử dụng các từ khóa thực sự (ví dụ: lessLeft thay vì LEFT_REDUCE). Sử dụng mũi tên toán học thực sự, thẻ mã khi bạn đang xử lý mã. Thích các ví dụ đầu vào / đầu ra hơn là giải thích mọi thứ. Tính toán trung gian làm cho nó khó đọc.
Mikaël Mayer

4

Đối với tập hợp x có các phần tử x0, x1, x2, x3 và một hàm tùy ý f bạn có các phần sau:

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

Tóm lại là

  • scangiống như foldnhưng cũng phát ra tất cả các giá trị trung gian
  • reduce không cần một giá trị ban đầu mà đôi khi khó tìm hơn một chút
  • fold cần một giá trị ban đầu khó tìm hơn một chút:
    • 0 cho tổng
    • 1 cho sản phẩm
    • phần tử đầu tiên cho min (một số có thể đề xuất Integer.MAX_VALUE)
  • không chắc chắn 100% nhưng có vẻ như có những triển khai tương đương này:
    • 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
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.