【问题标题】:StackOverflow in continuation monad延续单子中的 StackOverflow
【发布时间】:2012-06-25 11:47:12
【问题描述】:

使用以下延续单子:

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

我不明白为什么下面会出现堆栈溢出:

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)

虽然以下没有:

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)

【问题讨论】:

  • 请注意,我使用的是 VS 2012 RC,如果有人可以测试它在当前版本的 VS2010 上具有相同的行为。
  • 是的,它在 OCaml 中也有相同的行为。请参阅下面的答案。
  • FWIW,在 VS2015、F# 4.0、Update 3 中仍然可以观察到这种行为(尽管答案表明它不能归咎于编译器)。

标签: f# monads computation-expression


【解决方案1】:

要修复您的示例,请将此方法添加到您的 monad 定义中:

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

显然溢出的部分是map的递归调用中大输入列表的破坏。延迟它可以解决问题。

请注意,您的第二个版本将对 map 的递归调用置于另一个 let! 之后,后者对 Bind 和一个额外的 lambda 进行脱糖,实际上延迟了对 map 的递归调用。

在得出这个结论之前,我不得不寻找一些错误的线索。有帮助的是观察到StackOverflow 也被OCaml 抛出(尽管N 更高),除非递归调用被延迟。虽然F# TCO 有一些怪癖,但OCaml 更加成熟,所以这让我确信问题确实出在代码而不是编译器上:

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)

【讨论】:

  • 您可以在不添加延迟成员的情况下解决此问题 -- 请参阅我对 John Palmer 回复的评论。
  • @JackP.,我同意添加延迟成员不是唯一的解决方法。但是,您必须延迟输入列表的模式匹配,以便它不会完全发生在堆栈上。如果你不这样做,代码将会溢出(如果不在N=100000,那么在更高的N
【解决方案2】:

F# 编译器有时不是很聪明 - 在第一种情况下,它计算 map xs 然后 f x 然后加入它们,所以 map xs 不在尾部位置。在第二种情况下,它可以轻松地将map xs 重新排序到尾部位置。

【讨论】:

  • 我看不到如何在工作流生成的延续中以外的任何地方计算(f x)
  • @DavidGrenier -- 约翰是正确的。 (f x) 是在工作流生成的延续中计算的,但不是在它的 自己的 延续中计算的。也就是说,工作流只创建一个包裹(f x) :: xs 的延续——因此当调用该延续时,它不能对f 进行尾调用。当继续执行时,它必须在每次调用f 时推送一个堆栈帧,并且由于它是递归执行的,因此您将获得StackOverflowException。在您的第二个示例中,F# 编译器能够为所有内容生成尾调用。
  • @JackP 我不同意评论和您的回复。 map 立即返回,尾调用 map 不相关。 f 不是递归的,尾调用 f 也不相关。此外,故障不在于 F# 编译器,根据我的测试,OCaml 在这里也是如此。
  • 我现在明白约翰的评论在什么意义上是正确的。拥有map xs k,而不仅仅是map xs,在尾部位置是至关重要的。我只是换一种说法,因为 F# 编译器不会重新排序代码 - 这就是它的编写方式。
猜你喜欢
  • 2010-10-14
  • 2013-03-22
  • 2011-02-09
  • 2014-12-07
  • 1970-01-01
  • 2015-09-30
  • 2014-10-21
  • 2011-05-30
  • 2014-08-10
相关资源
最近更新 更多