2012-06-25 23 views
10

Sử dụng đơn nguyên tiếp tục sau:StackOverflow trong việc tiếp tục đơn nguyên

type ContinuationMonad() = 
    member this.Bind (m, f) = fun c -> m (fun a -> f a c) 
    member this.Return x = fun k -> k x 

let cont = ContinuationMonad() 

Tôi không thấy lý do tại sao những điều sau đây mang lại cho tôi một chồng tràn:

let map f xs = 
    let rec map xs = 
     cont { 
      match xs with 
      | [] -> return [] 
      | x :: xs -> 
       let! xs = map xs 
       return f x :: xs 
     } 
    map xs id;; 

let q = [1..100000] |> map ((+) 1) 

Trong khi sau không:

let map f xs = 
    let rec map xs = 
     cont { 
      match xs with 
      | [] -> return [] 
      | x :: xs -> 
       let! v = fun g -> g(f x) 
       let! xs = map xs 
       return v :: xs 
     } 
    map xs id;; 

let q = [1..100000] |> map ((+) 1) 
+0

Lưu ý rằng tôi trên VS 2012 RC, nếu có ai có thể kiểm tra nó có cùng một hành vi trên bản phát hành hiện tại của VS2010. –

+0

Có, và nó cũng có hành vi tương tự trong OCaml. Xem câu trả lời của tôi dưới đây. – t0yv0

+0

FWIW, hành vi này vẫn có thể được quan sát với VS2015, F # 4.0, Update 3 (mặc dù câu trả lời cho biết nó không thể đổ lỗi cho trình biên dịch). – Abel

Trả lời

7

Để khắc phục ví dụ của bạn, thêm phương pháp này để định nghĩa lại các đơn nguyên:

member this.Delay(mk) = fun c -> mk() c 

Rõ ràng phần mà tràn là sự hủy diệt của danh sách đầu vào lớn trong cuộc gọi đệ quy của map. Trì hoãn nó giải quyết vấn đề.

Lưu ý rằng phiên bản thứ hai của bạn đặt cuộc gọi đệ quy để map đằng sau một let! mà desugars để Bind và một lambda thêm, có hiệu lực kéo dài thời gian cuộc gọi đệ quy để map.

Tôi đã phải theo đuổi một vài đường mòn sai trước khi đạt được kết luận này. Những gì đã giúp được quan sát rằng StackOverflow được ném bởi OCaml là tốt (mặc dù ở một cao hơn N) trừ khi cuộc gọi đệ quy bị trì hoãn. Trong khi F# TCO có một số tật, OCaml là đã được chứng minh nhiều hơn, vì vậy đây đã thuyết phục tôi rằng vấn đề thực sự là với mã và không trình biên dịch:

let cReturn x = fun k -> k x 
let cBind m f = fun c -> m (fun a -> f a c) 

let map f xs = 
    (* inner map loop overflows trying to pattern-match long lists *) 
    let rec map xs = 
    match xs with 
     | [] -> cReturn [] 
     | x :: xs -> 
     cBind (map xs) (fun xs -> cReturn (f x :: xs)) in 
    map xs (fun x -> x) 

let map_fixed f xs = 
    (* works without overflowing by delaying the recursive call *) 
    let rec map xs = 
    match xs with 
     | [] -> cReturn [] 
     | x :: xs -> 
     cBind (fun c -> map xs c) (fun xs -> cReturn (f x :: xs)) in 
    map xs (fun x -> x) 

let map_fused f xs = 
    (* manually fused version avoids the problem by tail-calling `map` *) 
    let rec map xs k = 
    match xs with 
     | [] -> k [] 
     | x :: xs -> 
     map xs (fun xs -> k (f x :: xs)) in 
    map xs (fun x -> x) 
+1

Bạn có thể giải quyết vấn đề này mà không cần thêm thành viên Delay - xem bình luận của tôi về câu trả lời của John Palmer. –

+1

@JackP., Tôi đồng ý rằng việc thêm thành viên Trì hoãn không phải là bản sửa lỗi duy nhất. Tuy nhiên, bạn phải trì hoãn mẫu khớp với danh sách đầu vào để nó không xảy ra hoàn toàn trên ngăn xếp.Nếu bạn không làm điều đó, mã sẽ tràn (nếu không ở 'N = 100000' thì ở mức cao hơn' N'. – t0yv0

4

Trình biên dịch F # đôi khi không thông minh lắm - trong trường hợp đầu tiên nó tính map xs rồi f x và sau đó gia nhập chúng, vì vậy map xs không ở vị trí đuôi. Trong trường hợp thứ hai, nó có thể sắp xếp lại các map xs để ở vị trí đuôi dễ dàng.

+0

Tôi không thấy cách '(f x)' có thể được tính ở bất kỳ đâu nhưng bên trong sự tiếp tục được tạo ra bởi luồng công việc. –

+0

@DavidGrenier - John là chính xác. '(f x)' được tính bên trong sự tiếp tục được tạo ra bởi luồng công việc, nhưng nó không được tính bên trong đó là sự tiếp nối * riêng *. Nghĩa là, luồng công việc chỉ tạo ra một phép nối tiếp bao bọc '(f x) :: xs' - vì vậy khi tiếp tục được gọi, nó không thể thực hiện một cuộc gọi đuôi đến' f'. Khi sự tiếp tục đó được thực hiện, nó phải đẩy một khung ngăn xếp mỗi khi nó gọi 'f', và vì nó làm như vậy đệ quy bạn nhận được' StackOverflowException'. Trong ví dụ thứ hai của bạn, trình biên dịch F # có thể tạo ra các cuộc gọi đuôi cho mọi thứ. –

+0

@JackP Tôi không đồng ý với cả nhận xét và phản hồi của bạn. 'map' trả về ngay lập tức, gọi là' map' không liên quan. 'f' không đệ quy, đuôi gọi' f' không liên quan. Ngoài ra, lỗi không phải với trình biên dịch F #, OCaml cũng làm như vậy, dựa trên các thử nghiệm của tôi. – t0yv0

Các vấn đề liên quan