2012-06-25 21 views
10

Utilizzando il seguente prosecuzione monade:StackOverflow in continuazione monade

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() 

non riesco a capire perché il seguente mi dà un overflow dello stack:

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) 

Mentre il seguente non lo fa:

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

Si noti che io sono in VS 2012 RC, se qualcuno potesse testarlo ha lo stesso comportamento sulla versione corrente di VS2010. –

+0

Sì, e ha anche lo stesso comportamento in OCaml. Vedi la mia risposta qui sotto. – t0yv0

+0

FWIW, questo comportamento può ancora essere osservato con VS2015, F # 4.0, Update 3 (anche se le risposte indicano che non può essere incolpato nel compilatore). – Abel

risposta

7

per risolvere il tuo esempio, aggiungere questo metodo per la sua definizione della monade:

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

A quanto pare la parte che trabocca è la distruzione della lista di input di grandi dimensioni nella chiamata ricorsiva di map. Ritardare risolve il problema.

Nota che la seconda versione mette la chiamata ricorsiva a map dietro un'altra let! che desugars ad Bind ed un lambda in più, in effetti ritardare la chiamata ricorsiva a map.

Ho dovuto seguire alcune false piste prima di giungere a questa conclusione. Ciò che ha aiutato è stato osservare che StackOverflow viene generato anche da OCaml (anche se con un valore superiore a N) a meno che la chiamata ricorsiva non venga ritardata. Mentre F# TCO ha alcune stranezze, OCaml è più provata, quindi questo mi ha convinto che il problema è in effetti con il codice e non il compilatore:

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

Puoi risolvere questo problema senza aggiungere il membro Delay - guarda il mio commento sulla risposta di John Palmer. –

+1

@JackP., Sono d'accordo che l'aggiunta del membro Delay non è l'unica soluzione. Tuttavia, è necessario ritardare la corrispondenza del modello nell'elenco di input in modo che non avvenga interamente sullo stack.Se non lo fai, il codice traboccherà (se non a 'N = 100000' quindi a un' N' più alto. – t0yv0

4

Il compilatore F # a volte non è molto intelligente - nel primo caso calcola map xs quindi f x e quindi si uniscono a loro, quindi map xs non si trova in una posizione di coda. Nel secondo caso, è possibile riordinare lo map xs in posizione di coda facilmente.

+0

Non riesco a vedere come '(f x)' può essere calcolato ovunque ma all'interno della continuazione generata dal flusso di lavoro. –

+0

@DavidGrenier - John è corretto. '(f x)' è calcolato all'interno della continuazione generata dal flusso di lavoro, ma non è calcolato all'interno della sua * propria * continuazione. Cioè, il flusso di lavoro crea solo una continuazione che avvolge '(f x) :: xs' - così quando viene chiamata quella continuazione, non può effettuare una coda chiamata a' f'. Quando viene eseguita questa continuazione, deve premere un frame stack ogni volta che chiama 'f', e dal momento che lo fa in modo ricorsivo si ottiene' StackOverflowException'. Nel tuo secondo esempio, il compilatore F # è in grado di generare code per tutto. –

+0

@JackP Non concordo con il commento e la tua risposta. 'map' restituisce immediatamente, tail-calling' map' non è rilevante. 'f' non è ricorsivo, la coda chiamata' f' non è rilevante neanche. Inoltre, l'errore non è con il compilatore F #, OCaml fa lo stesso qui, in base ai miei test. – t0yv0