我有一个csv文件,其结构如下:
我编写了一个小脚本来遍历文件的每一行,并返回一系列元组,其中包含列标题和该列中最大字符串数据的长度:
let getColumnInfo (fileName:string) =
let delimiter = ','
let readLinesIntoColumns (sr:StreamReader) = seq {
while not sr.EndOfStream do
yield sr.ReadLine().Split(delimiter) |> Seq.map (fun c -> c.Length )
}
use sr = new StreamReader(fileName)
let headers = sr.ReadLine().Split(delimiter)
let columnSizes =
let initial = Seq.map ( fun h -> 0 ) headers
let toMaxColLengths (accumulator:seq<int>) (line:seq<int>) =
let chooseBigger a b = if a > b then a else b
Seq.map2 chooseBigger accumulator line
readLinesIntoColumns sr |> Seq.fold toMaxColLengths initial
Seq.zip headers columnSizes;
这适用于小文件。然而,当它试图处理一个大文件(> 75 Mb)时,它会使用StackOverflow异常来填充fsi。如果我删除该行
Seq.map2 chooseBigger accumulator line
程序完成。
现在,我的问题是:为什么F#用尽了堆栈?我对F#中的序列的理解是整个序列不保存在内存中,只保存在正在处理的元素中。因此,我预计已处理的行不会保留在堆栈中。我的误解在哪里?
答案 0 :(得分:6)
我认为这是一个很好的问题。这是一个更简单的复制品:
let test n =
[for i in 1 .. n -> Seq.empty]
|> List.fold (Seq.map2 max) Seq.empty
|> Seq.iter ignore
test
创建一系列空序列,按行计算max,然后迭代生成的(空)序列。你会发现n
的值很高,这会导致堆栈溢出,即使根本没有任何值可以迭代!
解释原因有点棘手,但这里有点刺痛。问题在于,当您对序列进行折叠时,Seq.map2
将返回一个新序列,该序列将其工作推迟到枚举之前。因此,当您尝试迭代生成的序列时,最终会回调到深层计算n
层的链。
正如丹尼尔解释的那样,你可以通过急切地评估结果序列(例如将其转换为列表)来逃避这种情况。
修改强>
这是尝试进一步解释出现了什么问题。当您致电Seq.map2 max s1 s2
时,s1
和s2
实际上都未被列举;你得到一个新的序列,当枚举时,它将枚举它们并比较产生的值。因此,如果我们执行以下操作:
let s0 = Seq.empty
let s1 = Seq.map2 max Seq.emtpy s0
let s2 = Seq.map2 max Seq.emtpy s1
let s3 = Seq.map2 max Seq.emtpy s2
let s4 = Seq.map2 max Seq.emtpy s3
let s5 = Seq.map2 max Seq.emtpy s4
...
然后对Seq.map2
的调用总是立即返回并使用常量堆栈空间。 然而,枚举s5需要枚举s4,这需要枚举s3等。这意味着枚举s99999将构建一个巨大的调用堆栈,看起来有点像:
...
(s99996's enumerator).MoveNext()
(s99997's enumerator).MoveNext()
(s99998's enumerator).MoveNext()
(s99999's enumerator).MoveNext()
我们会得到堆栈溢出。
答案 1 :(得分:2)
您的代码包含很多序列,很难推理。我猜这就是绊倒你的原因。你可以使这更加简单和有效(渴望并非全是坏事):
let getColumnInfo (fileName:string) =
let delimiter = ','
use sr = new StreamReader(fileName)
match sr.ReadLine() with
| null | "" -> Array.empty
| hdr ->
let cols = hdr.Split(delimiter)
let counts = Array.zeroCreate cols.Length
while not sr.EndOfStream do
sr.ReadLine().Split(delimiter)
|> Array.iteri (fun i fld ->
counts.[i] <- max counts.[i] fld.Length)
Array.zip cols counts
这假设所有行都是非空的并且具有相同的列数。
您可以通过将此行更改为:
来修复您的功能Seq.map2 chooseBigger accumulator line |> Seq.toList |> seq
答案 2 :(得分:1)
为什么F#用尽了堆栈?我对F#中的序列的理解是整个序列不保存在内存中,只保存在正在处理的元素中。因此,我预计已处理的行不会保留在堆栈中。我的误解在哪里?
线条本身并没有占用你的筹码空间。问题是你不小心编写了一个函数,它建立了一个巨大的未评估计算(thunk的树),当它被评估时堆栈溢出,因为它使非尾部调用O(n)深。每当您从其他序列构建序列并且不强制评估任何序列时,这往往会发生。