F#: Как правильно перечислить несколько файлов?

У меня есть куча файлов размером несколько мегабайт, которые очень просты:

  • Они имеют размер, кратный 8
  • Они содержат только двойные числа с прямым порядком байтов, поэтому их можно читать с помощью BinaryReader"s ReadDouble() метод

При лексикографической сортировке они содержат все значения в той последовательности, в которой они должны быть.

Я не могу хранить все в памяти как float list или же float array так что мне нужно float seq который просматривает необходимые файлы при фактическом доступе. Часть, которая проходит через последовательность, фактически делает это в императивном стиле, используя GetEnumerator() потому что я не хочу никаких утечек ресурсов и хочу закрыть все файлы правильно.

Мой первый функциональный подход был:

let readFile file = 
    let rec readReader (maybeReader : BinaryReader option) = 
        match maybeReader with
        | None -> 
            let openFile() = 
                printfn "Opening the file"
                new BinaryReader(new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read))
                |> Some
                |> readReader
            seq { yield! openFile() }
        | Some reader when reader.BaseStream.Position >= reader.BaseStream.Length -> 
            printfn "Closing the file"
            reader.Dispose()
            Seq.empty
        | Some reader -> 
            reader.BaseStream.Position |> printfn "Reading from position %d"
            let bytesToRead = Math.Min(1048576L, reader.BaseStream.Length - reader.BaseStream.Position) |> int
            let bytes = reader.ReadBytes bytesToRead
            let doubles = Array.zeroCreate<float> (bytesToRead / 8)
            Buffer.BlockCopy(bytes, 0, doubles, 0, bytesToRead)
            seq { 
                yield! doubles
                yield! readReader maybeReader
            }
    readReader None

А потом, когда у меня есть string list содержащий все файлы, я могу сказать что-то вроде:

let values = files |> Seq.collect readFile
use ve = values.GetEnumerator()
// Do stuff that only gets partial data from one file

Тем не менее, это только закрывает файлы, когда читатель достигает своего конца (что ясно при взгляде на функцию). В качестве второго подхода я реализовал файл, перечисляющий обязательно:

type FileEnumerator(file : string) = 
    let reader = new BinaryReader(new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read))
    let mutable _current : float = Double.NaN
    do file |> printfn "Enumerator active for %s"

    interface IDisposable with
        member this.Dispose() = 
            reader.Dispose()
            file |> printfn "Enumerator disposed for %s"

    interface IEnumerator with
        member this.Current = _current :> obj
        member this.Reset() = reader.BaseStream.Position <- 0L
        member this.MoveNext() = 
            let stream = reader.BaseStream
            if stream.Position >= stream.Length then false
            else 
                _current <- reader.ReadDouble()
                true

    interface IEnumerator<float> with
        member this.Current = _current

type FileEnumerable(file : string) = 

    interface IEnumerable with
        member this.GetEnumerator() = new FileEnumerator(file) :> IEnumerator

    interface IEnumerable<float> with
        member this.GetEnumerator() = new FileEnumerator(file) :> IEnumerator<float>

let readFile' file = new FileEnumerable(file) :> float seq

теперь, когда я говорю

let values = files |> Seq.collect readFile'
use ve = values.GetEnumerator()
// do stuff with the enumerator

Правильно распорядитель перечислителя подходит моему перечислителю.

Хотя это реальное решение для того, чего я хочу достичь (я мог бы сделать это быстрее, читая его по блокам, как первый функциональный подход, но для краткости я не делал этого здесь), мне интересно, есть ли действительно функциональный подход для избежания этого изменяемое состояние в перечислителе.

1 ответ

Решение

Я не совсем понимаю, что вы имеете в виду, когда говорите, что использование GetEnumerator() предотвратит утечку ресурсов и позволит правильно закрыть все файлы. Ниже будет моя попытка сделать это (игнорирование части блочного копирования в демонстрационных целях), и я думаю, что в результате файлы будут правильно закрыты.

let eof (br : BinaryReader) = 
  br.BaseStream.Position = br.BaseStream.Length  

let readFileAsFloats filePath = 
    seq{
        use file = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read)
        use reader = new BinaryReader(file)
        while (not (eof reader)) do
            yield reader.ReadDouble()
    }

let readFilesAsFloats filePaths = 
    filePaths |> Seq.collect readFileAsFloats

let floats = readFilesAsFloats ["D:\\floatFile1.txt"; "D:\\floatFile2.txt"]

Это то, что вы имели в виду?

Другие вопросы по тегам