2011-12-18 40 views
9

Sau đây blog article cho thấy làm thế nào trong F # foldBack có thể được thực hiện đệ quy đuôi bằng cách sử dụng tiếp tục đi qua phong cách.Có thể sử dụng phần tiếp tục để làm cho phần đuôi gập đệ quy không?

Trong Scala này sẽ có nghĩa là:

def foldBack[T,U](l: List[T], acc: U)(f: (T, U) => U): U = { 
    l match { 
    case x :: xs => f(x, foldBack(xs, acc)(f)) 
    case Nil => acc 
    } 
} 

thể được thực hiện đệ quy đuôi bằng cách làm này:

def foldCont[T,U](list: List[T], acc: U)(f: (T, U) => U): U = { 
    @annotation.tailrec 
    def loop(l: List[T], k: (U) => U): U = { 
    l match { 
     case x :: xs => loop(xs, (racc => k(f(x, racc)))) 
     case Nil => k(acc) 
    } 
    } 
    loop(list, u => u) 
} 

Thật không may, tôi vẫn nhận được một lỗi tràn stack cho danh sách dài. vòng lặp là đuôi đệ quy và tối ưu hóa nhưng tôi đoán sự tích lũy chồng chỉ là di chuyển vào các cuộc gọi tiếp tục.

Tại sao đây không phải là vấn đề với F #? Và có cách nào để giải quyết vấn đề này với Scala không?

Sửa: ở đây một số mã cho thấy chiều sâu của stack:

def showDepth(s: Any) { 
    println(s.toString + ": " + (new Exception).getStackTrace.size) 
} 

def foldCont[T,U](list: List[T], acc: U)(f: (T, U) => U): U = { 
    @annotation.tailrec 
    def loop(l: List[T], k: (U) => U): U = { 
    showDepth("loop") 
    l match { 
     case x :: xs => loop(xs, (racc => { showDepth("k"); k(f(x, racc)) })) 
     case Nil => k(acc) 
    } 
    } 
    loop(list, u => u) 
} 

foldCont(List.fill(10)(1), 0)(_ + _) 

in này:

loop: 50 
loop: 50 
loop: 50 
loop: 50 
loop: 50 
loop: 50 
loop: 50 
loop: 50 
loop: 50 
loop: 50 
loop: 50 
k: 51 
k: 52 
k: 53 
k: 54 
k: 55 
k: 56 
k: 57 
k: 58 
k: 59 
k: 60 
res2: Int = 10 
+0

Điều đó không có ý nghĩa. Bạn có một trường hợp thử nghiệm đơn giản? –

+0

@ DanielC.Sobral, xem mã tôi đã thêm và nó in. – huynhjl

Trả lời

4

Vấn đề là chức năng tiếp tục (racc => k(f(x, racc))) riêng của mình. Nó sẽ được tailcall tối ưu hóa cho toàn bộ doanh nghiệp này để làm việc, nhưng không phải là.

Scala không thể thực hiện tối ưu hóa tailcall cho cuộc gọi đuôi tùy ý, chỉ cho những người có thể biến thành vòng lặp (tức là khi chức năng tự gọi, không phải một số chức năng khác).

+0

Đó là những gì tôi đoán. Có điều gì có thể làm được không? Giống như sử dụng một cái gì đó giống như trampolines? – huynhjl

+0

Trampolines có thể sẽ giúp ích, nhưng tôi nghĩ trong trường hợp đặc biệt này, 'leftFold' sẽ giải quyết vấn đề với ít đau hơn. Nếu bạn vì lý do nào đó hoàn toàn phải có ngữ nghĩa 'foldRight', bạn có thể đảo ngược danh sách và gọi' foldLeft' trên kết quả. –

+1

Hóa ra nó thực sự không phải là đau đớn trong trường hợp này, xem câu trả lời của riêng tôi. – huynhjl

4

Tại sao đây không phải là vấn đề với F #?

F # có tất cả các cuộc gọi đuôi được tối ưu hóa.

Và có cách nào để giải quyết vấn đề này với Scala không?

Bạn có thể thực hiện TCO bằng các kỹ thuật khác như trampolines nhưng bạn bị mất interop vì nó thay đổi quy ước gọi và ~ 10 × chậm hơn. Đây là một trong ba lý do tôi không sử dụng Scala.

EDIT

kết quả benchmark của bạn chỉ ra rằng trampolines Scala của là nhanh hơn một nhiều hơn họ lần cuối cùng tôi thử nghiệm chúng. Ngoài ra, nó là thú vị để thêm điểm chuẩn tương đương bằng cách sử dụng F # và cho danh sách lớn hơn (vì không có điểm trong việc thực hiện CPS trên danh sách nhỏ!).

Đối 1,000x trên một danh sách 1.000 phần tử trên netbook của tôi với một 1.67GHz Intel Atom N570, tôi nhận được:

List.fold  0.022s 
List.rev+fold 0.116s 
List.foldBack 0.047s 
foldContTC 0.334s 

Đối 1x danh sách 1.000.000-yếu tố, tôi nhận được:

List.fold  0.024s 
List.rev+fold 0.188s 
List.foldBack 0.054s 
foldContTC 0.570s 

Bạn cũng có thể quan tâm đến các cuộc thảo luận cũ về điều này trên danh sách caml trong bối cảnh thay thế các hàm danh sách không đệ quy của OCAMl bằng các hàm đệ quy đuôi được tối ưu hóa.

+3

Hai lý do khác mà bạn không sử dụng Scala là gì? –

+3

@StephenSwensen: Thiếu các loại giá trị và thiếu suy luận kiểu. Lưu ý rằng việc thiếu các cuộc gọi đuôi và các loại giá trị là một vấn đề với JVM và không phải là Scala. Đây cũng là những lý do tại sao tôi chọn phát triển HLVM trên LLVM hơn là JVM. Dự án Geoff Reedy của cảng Scala để LLVM có tiềm năng để sửa chữa cả hai vấn đề này, mà sẽ là hoàn toàn tuyệt vời. –

6

Jon, n.m., cảm ơn bạn đã trả lời. Dựa trên ý kiến ​​của bạn tôi nghĩ tôi sẽ thử và sử dụng tấm bạt lò xo. Một chút nghiên cứu cho thấy Scala có hỗ trợ thư viện cho trampolines trong TailCalls. Dưới đây là những gì tôi đã đưa ra sau một chút quan trọng xung quanh:

def foldContTC[T,U](list: List[T], acc: U)(f: (T, U) => U): U = { 
    import scala.util.control.TailCalls._ 
    @annotation.tailrec 
    def loop(l: List[T], k: (U) => TailRec[U]): TailRec[U] = { 
    l match { 
     case x :: xs => loop(xs, (racc => tailcall(k(f(x, racc))))) 
     case Nil => k(acc) 
    } 
    } 
    loop(list, u => done(u)).result 
} 

Tôi đã quan tâm để xem cách này so sánh với các giải pháp mà không có tấm bạt lò xo cũng như mặc định foldLeftfoldRight triển khai. Đây là mã chuẩn và một số kết quả:

val size = 1000 
val list = List.fill(size)(1) 
val warm = 10 
val n = 1000 
bench("foldContTC", warm, lots(n, foldContTC(list, 0)(_ + _))) 
bench("foldCont", warm, lots(n, foldCont(list, 0)(_ + _))) 
bench("foldRight", warm, lots(n, list.foldRight(0)(_ + _))) 
bench("foldLeft", warm, lots(n, list.foldLeft(0)(_ + _))) 
bench("foldLeft.reverse", warm, lots(n, list.reverse.foldLeft(0)(_ + _))) 

Các timings là:

foldContTC: warming... 
Elapsed: 0.094 
foldCont: warming... 
Elapsed: 0.060 
foldRight: warming... 
Elapsed: 0.160 
foldLeft: warming... 
Elapsed: 0.076 
foldLeft.reverse: warming... 
Elapsed: 0.155 

Dựa trên điều này, nó sẽ có vẻ trampolining mà thực sự là năng suất hoạt động khá tốt. Tôi nghi ngờ rằng hình phạt trên đầu trang của boxing/unboxing là tương đối không phải là xấu.

Chỉnh sửa: theo đề xuất của nhận xét của Jon, dưới đây là thời gian trên 1M mục xác nhận rằng hiệu suất giảm xuống với danh sách lớn hơn. Ngoài ra tôi phát hiện ra rằng thực hiện List.foldLeft thư viện không overriden, vì vậy tôi timed với foldLeft2 sau:

def foldLeft2[T,U](list: List[T], acc: U)(f: (T, U) => U): U = { 
    list match { 
    case x :: xs => foldLeft2(xs, f(x, acc))(f) 
    case Nil => acc 
    } 
} 

val size = 1000000 
val list = List.fill(size)(1) 
val warm = 10 
val n = 2 
bench("foldContTC", warm, lots(n, foldContTC(list, 0)(_ + _))) 
bench("foldLeft", warm, lots(n, list.foldLeft(0)(_ + _))) 
bench("foldLeft2", warm, lots(n, foldLeft2(list, 0)(_ + _))) 
bench("foldLeft.reverse", warm, lots(n, list.reverse.foldLeft(0)(_ + _))) 
bench("foldLeft2.reverse", warm, lots(n, foldLeft2(list.reverse, 0)(_ + _))) 

sản lượng:

foldContTC: warming... 
Elapsed: 0.801 
foldLeft: warming... 
Elapsed: 0.156 
foldLeft2: warming... 
Elapsed: 0.054 
foldLeft.reverse: warming... 
Elapsed: 0.808 
foldLeft2.reverse: warming... 
Elapsed: 0.221 

Vì vậy foldLeft2.reverse là người chiến thắng ...

+0

"hiệu suất khá tốt". Thật. Tôi sẽ gọi hiệu suất tốt đáng ngờ đó! Có lẽ việc thực hiện tấm bạt lò xo là đủ thông minh để nhận ra rằng nó không phải đá vào bởi vì danh sách của bạn quá ngắn? Bạn nhận được các phép đo hiệu suất nào với danh sách thành phần 1M? –

+0

Với thời gian đóng, các vấn đề về bộ nhớ cache và GC cũng sẽ được phát, ví dụ: việc đảo ngược danh sách các phần tử 1k tương tự là rẻ với một GC thế hệ và bộ nhớ đệm hiệu quả nhưng các danh sách phần tử 1M có khả năng tồn tại trong vườn ươm hoặc khu vực địa phương, sẽ phải chịu chi phí và giảm hiệu quả bộ nhớ cache. –

+0

@JonHarrop, cuộc gọi tốt, thêm thời gian cho các yếu tố 1M. – huynhjl

3

Tôi trễ câu hỏi này, nhưng tôi muốn cho bạn thấy cách bạn có thể viết một FoldRight đuôi đệ quy mà không cần sử dụng tấm bạt lò xo đầy đủ; bằng cách tích lũy một danh sách các lần tiếp tục (thay vì yêu cầu họ gọi nhau khi thực hiện, dẫn đến tràn ngăn xếp) và xếp chúng vào cuối, giống như giữ một chồng, nhưng trên heap:

object FoldRight { 

    def apply[A, B](list: Seq[A])(init: B)(f: (A, B) => B): B = { 
    @scala.annotation.tailrec 
    def step(current: Seq[A], conts: List[B => B]): B = current match { 
     case Seq(last) => conts.foldLeft(f(last, init)) { (acc, next) => next(acc) } 
     case Seq(x, xs @ _*) => step(xs, { acc: B => f(x, acc) } +: conts) 
     case Nil => init 
    } 
    step(list, Nil) 
    } 

} 

Các nếp gấp xảy ra ở cuối chính nó là đệ quy đuôi. Hãy dùng thử in ScalaFiddle

Về mặt hiệu suất, nó hoạt động hơi tồi tệ hơn phiên bản cuộc gọi đuôi.

[info] Benchmark   (length) Mode Cnt Score Error Units 
[info] FoldRight.conts   100 avgt 30 0.003 ± 0.001 ms/op 
[info] FoldRight.conts   10000 avgt 30 0.197 ± 0.004 ms/op 
[info] FoldRight.conts  1000000 avgt 30 77.292 ± 9.327 ms/op 
[info] FoldRight.standard  100 avgt 30 0.002 ± 0.001 ms/op 
[info] FoldRight.standard  10000 avgt 30 0.154 ± 0.036 ms/op 
[info] FoldRight.standard 1000000 avgt 30 18.796 ± 0.551 ms/op 
[info] FoldRight.tailCalls  100 avgt 30 0.002 ± 0.001 ms/op 
[info] FoldRight.tailCalls  10000 avgt 30 0.176 ± 0.004 ms/op 
[info] FoldRight.tailCalls 1000000 avgt 30 33.525 ± 1.041 ms/op 
Các vấn đề liên quan