Search code examples
f#printfdisposeobjectdisposedexception

How can I write an fprintfn function that opens, appends, closes the file on each write, without getting an ObjectDisposedException?


TL;DR: how to write a function like fprintfn that accesses and closes a file and behaves similar to printf family of functions, but won't throw an ObjectDisposedException on multiple arguments?


As a convenience embedded function, I found myself writing the following code, which worked for a while:

let fprintfn a b = 
    use handle = File.AppendText("somelogfile.log")
    printfn a b |> ignore   // write to stdout
    fprintfn handle a b     // write to logfile

// works
fprintfn "Here we are, %i years of age" 42

// throws ObjectDisposedException
fprintfn "Here we are, %i years of age in $i" 42 DateTime.Now.Year

The error is raised whether I use use or using and as soon as the number of arguments exceeds 2.

After some head-scratching, I concluded that the new fprintfn function above creates a closure as soon as I use it with more than one print format argument. Which seems to make sense to some extend, albeit a bit more hidden then in cases where you return a closure explicitly (i.e. returning an actual fun x -> something which accesses the handle variable).

Now the question is: how can I rewrite the above statement while retaining the convenience of the original fprintfn function use and syntax, without having it throw the ObjectDisposedException?


PS: A preferred way of writing the above function would be to use the following, which allows all fprintfn syntax, but that will throw the same exception already when you use a single print format argument.

let fprintfn a = 
    use handle = File.AppendText("somelogfile.log")
    printfn a |> ignore
    fprintfn handle a

fprintf "Test"              // works
fprintf "Test: %i" 42       // throws ODE

Solution

  • You are right that fprintf creates a continuation and returns it, so that by the time you call that continuation, the file is already closed.

    But you can go one level deeper and use kprintf. It lets you provide a "continuation" - i.e. a function that receives the formatted string and does whatever with it.

    let fprintfn a = 
      let doPrint s = 
        use handle = File.AppendText("somelogfile.log")
        printfn "%s" s
        fprintfn handle "%s" s
    
      kprintf doPrint a
    

    And then, of course, you can simplify that a bit by using File.AppendAllText:

    let fprintfn a = 
      let doPrint s = 
        File.AppendAllText("somelogfile.log", s + "\n")
        printfn "%s" s
      kprintf doPrint a