2012-06-25 23 views
10

Usando la siguiente mónada continuación:Stackoverflow en mónada continuació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() 

no veo por qué el siguiente me da un desbordamiento de pila:

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) 

Aunque la siguiente no:

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

Tenga en cuenta que estoy en VS 2012 RC, si alguien puede probar que tiene el mismo comportamiento en la versión actual de VS2010. –

+0

Sí, y también tiene el mismo comportamiento en OCaml. Ver mi respuesta a continuación. – t0yv0

+0

FWIW, este comportamiento todavía se puede observar con VS2015, F # 4.0, Actualización 3 (aunque las respuestas indican que no se puede culpar al compilador). – Abel

Respuesta

7

para fijar su ejemplo, agregue este método para su definición de la mónada:

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

Al parecer, la parte que se desborda es la destrucción de la lista de entrada grande en la llamada recursiva de map. Retrasarlo resuelve el problema.

en cuenta que su segunda versión pone la llamada recursiva a map detrás de otro, que let! desugars a Bind y un lambda adicional, en efecto, el retraso de la llamada recursiva a map.

Tuve que buscar algunas pistas falsas antes de llegar a esta conclusión. Lo que ayudó fue observar que StackOverflow también es lanzado por OCaml (aunque a un valor más alto N) a menos que la llamada recursiva se demore. Mientras F# TCO tiene algunas peculiaridades, OCaml es más probada, por lo que este me ha convencido de que el problema es de hecho con el código y no el compilador:

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

Puede resolver este problema sin agregar el miembro Delay: vea mi comentario sobre la respuesta de John Palmer. –

+1

@JackP., Acepto que agregar el miembro Delay no es la única solución. Sin embargo, debe retrasar la coincidencia de patrones en la lista de entrada para que no suceda completamente en la pila.Si no lo haces, el código se desbordará (si no está en 'N = 100000' y luego en una' N' más alta). – t0yv0

4

El compilador F # a veces no es muy inteligente - en el primer caso calcula map xs luego f x y luego se une a ellos, por lo que map xs no está en posición de cola. En el segundo caso, puede reordenar el map xs para estar en posición de cola fácilmente.

+0

No veo cómo se puede calcular '(f x)' en cualquier lugar, pero dentro de la continuación generada por el flujo de trabajo. –

+0

@DavidGrenier - John tiene razón. '(f x)' se calcula dentro de la continuación generada por el flujo de trabajo, pero no se calcula dentro de su * propia * continuación. Es decir, el flujo de trabajo solo crea una continuación que envuelve '(f x) :: xs', por lo que cuando se invoca esa continuación, no puede hacer una llamada final a' f'. Cuando se ejecuta esa continuación, tiene que empujar un marco de pila cada vez que llama a 'f', y como lo está haciendo recursivamente, obtienes la' StackOverflowException'. En su segundo ejemplo, el compilador F # puede generar llamadas finales para todo. –

+0

@JackP No estoy de acuerdo tanto con el comentario como con su respuesta. 'map' regresa inmediatamente, tail-calling' map' no es relevante. 'f' no es recursivo, tail-calling' f' tampoco es relevante. Además, la falla no está en el compilador F #, OCaml hace lo mismo aquí, según mis pruebas. – t0yv0