我有我为 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},...})。
let printList l =
Seq.iter (fun n -> printf "%i" n) l
printfn ""
let rec morris s =
let next str = seq {
let cnt = ref 1 // Stack overflow is below when enumerating
for cur in [|0|] |> Seq.append str |> Seq.windowed 2 do
if cur.[0] <> cur.[1] then
yield!( [!cnt ; cur.[0]] )
cnt := 0
incr cnt
}
seq {
yield s
yield! morris (next s) // tail recursion, no stack overflow
}
// "main"
// Print the nth iteration
let _ = [1] |> morris |> Seq.nth 3125 |> printList
您可以使用 Seq.nth 选择第 n 次迭代,但您只能在遇到堆栈溢出之前完成此操作。我有一点递归是尾递归,它本质上构建了一组链接的枚举器。这不是问题所在。这是在第 4000 个序列上调用“枚举”的时候。请注意,对于 F# 1.9.6.16,之前的版本最高超过 14000)。这是因为链接序列的解析方式。序列是惰性的,因此“递归”是惰性的。也就是说, seq n 调用 seq n-1 ,后者调用 seq n-2 等等以获取第一项(第一个 # 是最坏的情况)。
我知道,[|0|] |> Seq.append str |> Seq.windowed 2
这使我的问题变得更糟,如果我消除它,我可以产生三倍的#。实际上,代码运行良好。morris 的第 3125 次迭代长度将超过 10^359 个字符。
我真正想要解决的问题是如何保留惰性 eval,并且对于我可以选择的迭代没有基于堆栈大小的限制。我正在寻找合适的 F# 习惯用法来根据内存大小进行限制。
2010 年 10 月更新
在更好地学习 F#,一点点 Haskell,思考和研究这个问题一年多之后,我终于可以回答我自己的问题了。但与难题一样,问题始于错误的问题。问题不在于序列的序列——这实际上是因为递归定义的序列。我的函数式编程技能现在稍微好一点,所以更容易看到下面的版本发生了什么,它仍然得到一个 stackoverflow
let next str =
Seq.append str [0]
|> Seq.pairwise
|> Seq.scan (fun (n,_) (c,v) ->
if (c = v) then (n+1,Seq.empty)
else (1,Seq.ofList [n;c]) ) (1,Seq.empty)
|> Seq.collect snd
let morris = Seq.unfold(fun sq -> Some(sq,next sq))
这基本上创建了一个非常长的 Seq 处理函数调用链来生成序列。F#自带的Seq模块就是不使用栈就不能跟链的。它对追加和递归定义的序列进行了优化,但该优化仅在递归实现追加时才有效。
所以这会起作用
let rec ints n = seq { yield n; yield! ints (n+1) }
printf "%A" (ints 0 |> Seq.nth 100000);;
这个会得到一个stackoverflow。
let rec ints n = seq { yield n; yield! (ints (n+1)|> Seq.map id) }
printf "%A" (ints 0 |> Seq.nth 100000);;
为了证明 F# 库是问题所在,我编写了自己的 Seq 模块,该模块使用延续实现了追加、成对、扫描和收集,现在我可以毫无问题地开始生成和打印 50,000 个序列(它永远不会完成,因为它已经结束了10^5697 位长)。
一些附加说明:
- Continuations 是我一直在寻找的习语,但在这种情况下,它们必须进入 F# 库,而不是我的代码。我从Tomas Petricek 的 Real-World Functional Programming一书中了解了 F# 中的延续。
- 我接受的惰性列表答案是另一个成语;懒惰的评价。在我重写的库中,我还必须利用惰性类型来避免 stackoverflow。
- 惰性列表版本是靠运气工作的(可能是设计使然,但这超出了我目前的能力范围)——它在构建和迭代时使用的活动模式匹配导致列表在所需的递归变得太深之前计算值,所以它很懒,但不是那么懒,它需要继续以避免stackoverflow。例如,当第二个序列需要第一个序列中的一个数字时,它已经被计算出来了。换句话说,LL 版本对于序列生成并不是严格的 JIT 惰性,只是列表管理。