How to handle exceptions in F# when using sequence expressions?

Viewed 187

I am trying to write a F# function that reads a CSV file and returns its lines as a sequence of strings that can be further processed in a pipelined expression. The function should handle all exceptions that can arise when opening and reading a file. This is what I came up with so far:

// takes a filename and returns a sequence of strings
// returns empty sequence in case file could not be opened
let readFile (f : string) = 
  try 
    seq {
      use r = new StreamReader(f) // if this throws, exception is not caught below 
      while not r.EndOfStream do
        yield reader.ReadLine()   // same here 
    }
    with ex
        | ex when (ex :? Exception) -> 
            printfn "Exception: %s" ex.Message
            Seq.empty

The problem here is that the exceptions that could be thrown by StreamReader() and ReadLine() are not caught in the exception handler but instead are left uncaught and lead to program termination. Also, there seems to be no way of trying to catch exceptions inside the seq {} sequence expression. Right now I cannot think of any other way to design such a function than reading the whole file into an intermediate collection like a list or an array beforehand and then returning this collection as a sequence to the callers, thereby loosing all the benefits of lazy evaluation.

Has anybody got a better idea ?

2 Answers

The reason the exceptions are not caught by the try-with handler here is that the body of the seq is lazily executed. readFile returns the sequence without generating an exception, but then trying to execute that sequence generates an exception in the context where it is being used.

Since F# doesn't let you use try-with within a sequence expression, you have to be a bit creative here. You could use Seq.unfold to generate the sequence like so, for instance:

let readFile (f: string) =
    try
        new StreamReader(f)
        |> Seq.unfold
            (fun reader ->
                try
                    if not reader.EndOfStream then
                        Some(reader.ReadLine(), reader)
                    else
                        reader.Dispose()
                        None
                with ex ->
                    printfn "Exception while reading line: %O" ex
                    reader.Dispose()
                    None)
    with ex ->
        printfn "Exception while opening the file: %O" ex
        Seq.empty

Perhaps a less tricky approach would be to wrap StreamReader.ReadLine so that it doesn't throw exceptions. That way you can still use a seq expression and a use statement.

let readLine (reader: StreamReader) =
    try
        reader.ReadLine() |> Some
    with ex ->
        printfn "Exception while reading line: %O" ex
        None

let readFile2 (f: string) =
    try
        let r = new StreamReader(f)

        seq {
            use reader = r
            let mutable error = false

            while not error && not reader.EndOfStream do
                let nextLine = readLine reader
                if nextLine.IsSome then yield nextLine.Value else error <- true
        }
    with ex ->
        printfn "Exception while opening the file: %O" ex
        Seq.empty
let readFile (f : string) = 
    try File.ReadLines(f)
    with ex -> printfn "Exception: %s" ex.Message; Seq.empty
Related