避免堆栈溢出(使用F#无限序列)

避免堆栈溢出(使用F#无限序列),f#,stack-overflow,tail-recursion,sequences,F#,Stack Overflow,Tail Recursion,Sequences,我为f#中的morris seq编写了这个“学习代码”,它遭受堆栈溢出,我不知道如何避免。“morris”返回“see and say”序列的无限序列(即,{1}、{1,1}、{2,1}、{1,2,1,1}、{1,1,1,2,2,1}、{3,1,2,2,1,1}、{3,1,1,1}、}) 这基本上创建了一个非常长的Seq处理函数调用链来生成sequnce。F#附带的Seq模块是不使用堆栈就无法遵循链的模块。它对追加和递归定义的序列使用了一种优化,但这种优化只有在递归实现追加时才有效 所以这会起作

我为f#中的morris seq编写了这个“学习代码”,它遭受堆栈溢出,我不知道如何避免。“morris”返回“see and say”序列的无限序列(即,{1}、{1,1}、{2,1}、{1,2,1,1}、{1,1,1,2,2,1}、{3,1,2,2,1,1}、{3,1,1,1}、})

这基本上创建了一个非常长的Seq处理函数调用链来生成sequnce。F#附带的Seq模块是不使用堆栈就无法遵循链的模块。它对追加和递归定义的序列使用了一种优化,但这种优化只有在递归实现追加时才有效

所以这会起作用

let rec ints n = seq { yield n; yield! ints (n+1) }
printf "%A" (ints 0 |> Seq.nth 100000);;
这一个会有堆栈溢出

let rec ints n = seq { yield n; yield! (ints (n+1)|> Seq.map id) }
printf "%A" (ints 0 |> Seq.nth 100000);;
为了证明F#libary是问题所在,我编写了自己的Seq模块,该模块使用continuations实现了append、pairwise、scan和collect,现在我可以毫无问题地开始生成和打印50000个Seq(因为它的长度超过10^5697位,所以永远不会完成)

一些补充说明:

  • 延续是我一直在寻找的习惯用法,但在本例中,它们必须进入F#库,而不是我的代码。我从现实世界的函数式编程书中学到了F#中的连续体
  • 我接受的懒散列表的答案包含了另一个习语;惰性评估。在我重写的库中,我还必须利用惰性类型来避免堆栈溢出
  • lazy list版本靠运气工作(可能是设计的,但这超出了我目前的判断能力)-它在构造和迭代时使用的活动模式匹配会导致列表在所需递归太深之前计算值,因此它是懒惰的,但不是很懒惰,它需要继续以避免堆栈溢出。例如,当第二个序列需要第一个序列中的数字时,它已经被计算出来了。换句话说,LL版本对于序列生成不是严格的JIT惰性,只是列表管理

您一定要退房

但我稍后会尝试发布一个更全面的答案

更新

好的,下面是一个解决方案。它将Morris序列表示为int的懒散列表的懒散列表,因为我假设您希望它在“两个方向”上都是懒散的

F#LazyList(在FSharp.PowerPack.dll中)有三个有用的属性:

  • 它是惰性的(在第一次需要时才会对第n个元素进行求值)
  • 它不会重新计算(在同一对象实例上重新计算第n个元素不会重新计算它-它会在第一次计算后缓存每个元素)
  • 您可以“忘记”前缀(当您“尾随”到列表中时,不再引用的前缀可用于垃圾收集)
第一个属性与seq(IEnumerable)相同,但其他两个属性是LazyList所独有的,对于诸如本问题中提出的计算问题非常有用

不用多说,代码:

// print a lazy list up to some max depth
let rec PrintList n ll =
    match n with
    | 0 -> printfn ""
    | _ -> match ll with
           | LazyList.Nil -> printfn ""
           | LazyList.Cons(x,xs) ->
               printf "%d" x
               PrintList (n-1) xs

// NextMorris : LazyList<int> -> LazyList<int>
let rec NextMorris (LazyList.Cons(cur,rest)) = 
    let count = ref 1
    let ll = ref rest
    while LazyList.nonempty !ll && (LazyList.hd !ll) = cur do
        ll := LazyList.tl !ll
        incr count
    LazyList.cons !count
        (LazyList.consf cur (fun() ->
            if LazyList.nonempty !ll then
                NextMorris !ll
            else
                LazyList.empty()))

// Morris : LazyList<int> -> LazyList<LazyList<int>>
let Morris s =
    let rec MakeMorris ll =
        LazyList.consf ll (fun () ->
            let next = NextMorris ll
            MakeMorris next
        )
    MakeMorris s

// "main"
// Print the nth iteration, up to a certain depth
[1] |> LazyList.of_list |> Morris |> Seq.nth 3125 |> PrintList 10
[1] |> LazyList.of_list |> Morris |> Seq.nth 3126 |> PrintList 10
[1] |> LazyList.of_list |> Morris |> Seq.nth 100000 |> PrintList 35
[1] |> LazyList.of_list |> Morris |> Seq.nth 100001 |> PrintList 35

内存使用率保持不变(在我的机箱上低于16M)。。。我还没跑完,但我计算第55个长度的速度很快,即使是在我的慢速箱上,所以我认为这应该可以很好地工作。还要注意,我使用'bignum'表示长度,因为我认为这将溢出'int'。

只需保存您查找的上一个元素即可

let morris2 data = seq {
    let cnt = ref 0
    let prev = ref (data |> Seq.nth 0)

     for cur in data do
        if cur <> !prev then
            yield! [!cnt; !prev]
            cnt := 1
            prev := cur
        else
            cnt := !cnt + 1

    yield! [!cnt; !prev]
}

let rec morrisSeq2 cur = seq {
    yield cur
    yield! morrisSeq2 (morris2 cur)
}
2数据=seq{
设cnt=ref 0
设prev=ref(数据|>序号0)
对于数据中的cur,请执行以下操作:
如果cur!prev那么
收益率![!cnt;!prev]
cnt:=1
上一个:=当前
其他的
cnt:=!cnt+1
收益率![!cnt;!prev]
}
设rec-seq2 cur=seq{
屈服电流
屈服!Morriseq2(morris2 cur)
}

我认为这里有两个主要问题:

  • 懒惰是非常低效的,因此您可以期望懒惰的函数实现运行速度慢几个数量级。例如,所描述的Haskell实现比下面给出的F#慢2400倍。如果你想要一个解决方案,你最好的办法可能是通过将计算集中到一个迫切需要的批次中来分期付款,这些批次是按需生产的

  • Seq.append
    函数实际上是从
    IEnumerable
    调用C#代码,因此,它的尾部调用没有被消除,并且每次遍历时都会泄漏更多的堆栈空间。当您在序列上进行枚举时,就会出现这种情况

在计算第50个子序列的长度时,以下速度比您的实现快80倍以上,但可能对您来说还不够慢:

let next (xs: ResizeArray<_>) =
  let ys = ResizeArray()
  let add n x =
    if n > 0 then
      ys.Add n
      ys.Add x
  let mutable n = 0
  let mutable x = 0
  for i=0 to xs.Count-1 do
    let x' = xs.[i]
    if x=x' then
      n <- n + 1
    else
      add n x
      n <- 1
      x <- x'
  add n x
  ys

let morris =
  Seq.unfold (fun xs -> Some(xs, next xs)) (ResizeArray [1])
let next(xs:ResizeArray)=
设ys=ResizeArray()
让我们加nx=
如果n>0,则
加上
加上x
设可变n=0
设可变x=0
对于i=0到xs.Count-1 do
设x'=xs[i]
如果x=x’,那么

是的,我明白了,正如我在问题中提到的。你只是在延迟溢出。该限制仍然是基于堆栈的,而是发生在14000以上。对我来说,你已经用seq.nth杀死了懒惰的eval,所以我不得不重写一点来运行它。我希望它不仅能增加深度,而且能在内存不足而不是堆栈溢出的情况下失败。你的算法需要多长时间来计算第60个莫里斯元素?我不知道确切的时间。大概4分钟多。我的同事之一的C++版本是秒秒。功能越强大,速度就越慢。这是所有的对象创建。上面的版本立即开始创建输出,即使是14000。这个版本无论如何都不太实用。我在Haskell中以一种纯函数的方式写了这篇文章,这是a)更简洁(仅列表+模式匹配)和b)更快;-)我首先创建了一个列表版本。它的速度更快(60次为34秒),但占用了太多内存,我无法计算大于64次迭代的任何内容。我确实做了一个完整的功能版本(没有可变项),到第五天时速度太慢了
let LLLength ll =
    let rec Loop ll acc =
        match ll with
        | LazyList.Cons(_,rest) -> Loop rest (acc+1N)
        | _ -> acc
    Loop ll 0N

let Main() =
    // don't do line below, it leaks
    //let hundredth = [1] |> LazyList.of_list |> Morris |> Seq.nth 100
    // if we only want to count length, make sure we throw away the only
    // copy as we traverse it to count
    [1] |> LazyList.of_list |> Morris |> Seq.nth 100
        |> LLLength |> printfn "%A" 
Main()    
let morris2 data = seq {
    let cnt = ref 0
    let prev = ref (data |> Seq.nth 0)

     for cur in data do
        if cur <> !prev then
            yield! [!cnt; !prev]
            cnt := 1
            prev := cur
        else
            cnt := !cnt + 1

    yield! [!cnt; !prev]
}

let rec morrisSeq2 cur = seq {
    yield cur
    yield! morrisSeq2 (morris2 cur)
}
let next (xs: ResizeArray<_>) =
  let ys = ResizeArray()
  let add n x =
    if n > 0 then
      ys.Add n
      ys.Add x
  let mutable n = 0
  let mutable x = 0
  for i=0 to xs.Count-1 do
    let x' = xs.[i]
    if x=x' then
      n <- n + 1
    else
      add n x
      n <- 1
      x <- x'
  add n x
  ys

let morris =
  Seq.unfold (fun xs -> Some(xs, next xs)) (ResizeArray [1])