【问题标题】:F# Writing to file changes behavior on return typeF# 写入文件更改返回类型的行为
【发布时间】:2016-12-14 13:24:04
【问题描述】:

我有以下函数可以将 csv 文件转换为特定的 txt 模式(CNTKTextFormat Reader 预期):

open System.IO
open FSharp.Data;
open Deedle;

let convert (inFileName : string) = 
    let data = Frame.ReadCsv(inFileName)
    let outFileName = inFileName.Substring(0, (inFileName.Length - 4)) + ".txt"
    use outFile = new StreamWriter(outFileName, false)
    data.Rows.Observations
    |> Seq.map(fun kvp ->
        let row = kvp.Value |> Series.observations |> Seq.map(fun (k,v) -> v) |> Seq.toList
        match row with
        | label::data ->
            let body = data |> List.map string |> String.concat " "
            outFile.WriteLine(sprintf "|labels %A |features %s" label body)
            printf "%A" label
        | _ ->
            failwith "Bad data."
    )
    |> ignore

奇怪的是,在 F# 交互式面板中运行后,输出文件为空,printf 根本不打印。

如果我删除 ignore 以确保正在处理实际行(通过返回空值序列来证明),而不是得到一个空文件:

val it : seq<unit> = Error: Cannot write to a closed TextWriter.

之前,我使用 let 声明 StreamWriter 并手动处理它,但我也生成了空文件或仅生成几行(比如千分之五)。

这里发生了什么?另外,如何解决文件写入问题?

【问题讨论】:

  • Seq.map 很懒惰。你想要的是Seq.iter,它接受一个副作用函数(一个返回unit),并且,非懒惰地,将它应用于序列中的每个项目。

标签: f# cntk


【解决方案1】:

Seq.map 返回一个惰性序列,在迭代之前不会对其进行评估。您当前没有在 convert 内对其进行迭代,因此不会处理任何行。如果您返回 Seq<unit> 并在 convert 之外对其进行迭代,则 outFile 将已经关闭,这就是您看到异常的原因。

您应该改用Seq.iter

data.Rows.Observations
    |> Seq.iter (fun kvp -> ...)

【讨论】:

    【解决方案2】:

    除了已经提到的解决方案之外,您还可以完全避免使用StreamWriter,并使用标准的.Net 函数之一,File.WriteAllLines。您将准备一系列转换后的行,然后将其写入文件:

    let convert (inFileName : string) = 
        let lines = 
            Frame.ReadCsv(inFileName).Rows.Observations
            |> Seq.map(fun kvp ->
                let row = kvp.Value |> Series.observations |> Seq.map snd |> Seq.toList
                match row with
                | label::data ->
                    let body = data |> List.map string |> String.concat " "
                    printf "%A" label
                    sprintf "|labels %A |features %s" label body
                | _ ->
                    failwith "Bad data."
            )
        let outFileName = inFileName.Substring(0, (inFileName.Length - 4)) + ".txt"
        File.WriteAllLines(outFileName, lines)
    

    更新基于 cmets 中的讨论:这是一个完全避免 Deedle 的解决方案。根据您今天发布的另一个问题,我在这里对您的输入文件格式做出了一些假设:标签在第 1 列,功能如下。

    let lines = 
        File.ReadLines inFileName
        |> Seq.map (fun line -> 
            match Seq.toList(line.Split ',') with
            | label::data ->
                let body = data |> List.map string |> String.concat " "
                printf "%A" label
                sprintf "|labels %A |features %s" label body
            | _ ->
                failwith "Bad data."
        )
    

    【讨论】:

    • 很酷的事情是:你不必担心文件大小,序列是惰性的(好吧,至少如果 ReadCsv 正在做所有正确的事情,我认为它确实如此)
    • 天哪,我什么都没学到。有没有办法在输入文件行中进行这种延迟加载?
    • File.ReadLines 是你的朋友
    【解决方案3】:

    正如 Lee 已经提到的,Seq.map 很懒惰。这也是为什么您会收到“无法写入已关闭的 TextWriter”的原因:use 关键字在超出范围时会处理其 IDisposable。在这种情况下,这是您的功能的结尾。由于 Seq.map 是惰性的,因此您的函数返回了一个 unevalated 序列对象,该对象已在您的 use 语句中关闭了 StreamWriter ——但当您评估该序列时(无论您的部分代码检查了 Seq 的空值,或者在 F# 交互窗口中),StreamWriter 已经因超出范围而被释放。

    Seq.map改成Seq.iter,你的两个问题都解决了。

    【讨论】:

      猜你喜欢
      • 2011-11-09
      • 1970-01-01
      • 2019-11-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-07-03
      • 1970-01-01
      • 2017-12-18
      相关资源
      最近更新 更多