F# has Handy GC
As mentioned previously I was recently learning about F#, a neat mostly functional language for the .NET vm.
One of the things I was really impressed with was that it allows the user
to take advantage of timely destructors. I was under the impression that
except for reference counted GC (perl, cpython, and I think C++) timely
destructors were impossible and that the user is instead required to close
their filehandles, database handles, or whatever other cleanup they need to
do, within a finally
block.
When reading the Book of F# I realized that C# and other .NET langauges can probably do this as well, since the IDisposable interface is how it’s referenced.
Unlike perl, for example, where a destructor, when defined, is always called when the object goes out of scope, in F# it’s up to the user to decide that it’s important to call it immediately. So for example, the user might know that it’s ok if a file is closed later than the block ending because it’s some kind of one off logging, but a database handle must be closed immediately for a transaction to complete or something.
🔗 use
F# gives the user two ways to do this. The first is with the use
keyword, or
binding type.
type DisposableHuman (name : string) =
do printfn "Creating person: %s" name
member x.Name = name
interface System.IDisposable with
member x.Dispose() =
printfn "disposing: %s" name
let testDisposable() =
use root = new DisposableHuman("outer")
for i in [1..2] do
use nested = new DisposableHuman(sprintf "inner %i" i)
printfn "completing iteration %i" i
printfn "leaving function"
testDisposable ()
The output is:
creating: outer
creating: inner 1
completing iteration 1
disposing: inner 1
creating: inner 2
completing iteration 2
disposing: inner 2
leaving function
disposing: outer
So check this out, if the user does not use use
and instead opts to use
let
, the more typical binding, the destructors never get called:
type DisposableHuman (name : string) =
do printfn "Creating person: %s" name
member x.Name = name
interface System.IDisposable with
member x.Dispose() =
printfn "disposing: %s" name
let testDisposable() =
let root = new DisposableHuman("outer")
for i in [1..2] do
let nested = new DisposableHuman(sprintf "inner %i" i)
printfn "completing iteration %i" i
printfn "leaving function"
testDisposable ()
and that output is:
creating: outer
creating: inner 1
completing iteration 1
creating: inner 2
completing iteration 2
leaving function
Of course in that case the user is at fault for not calling dispose by hand, like this:
type DisposableHuman (name : string) =
do printfn "Creating person: %s" name
member x.Name = name
member x.Teardown() =
printfn "disposing: %s" name
interface System.IDisposable with
member x.Dispose() = x.Teardown ()
let testDisposable() =
let root = new DisposableHuman("outer")
for i in [1..2] do
let nested = new DisposableHuman(sprintf "inner %i" i)
printfn "completing iteration %i" i
nested.Teardown ()
printfn "leaving function"
root.Teardown ()
testDisposable ()
And then the output we get is what we saw the first time:
creating: outer
creating: inner 1
completing iteration 1
disposing: inner 1
creating: inner 2
completing iteration 2
disposing: inner 2
disposing: outer
leaving function
🔗 using
Alternately, if the user has a more decomposed task, they can use the
using
binding, which as far as I can tell uses function scoping instead
of block scoping.
type DisposableHuman (name : string) =
do printfn "Creating person: %s" name
member x.Name = name
interface System.IDisposable with
member x.Dispose() =
printfn "disposing: %s" name
let testDisposable() =
use root = new DisposableHuman("outer")
for i in [1..2] do
let name = using (new DisposableHuman(sprintf "inner %i" i)) (fun disp -> disp.Name)
printfn "got name %s" name
printfn "leaving function"
testDisposable ()
And then, the output:
Creating person: outer
Creating person: inner 1
disposing: inner 1
got name inner 1
Creating person: inner 2
disposing: inner 2
got name inner 2
leaving function
disposing: outer
I’m not completely sure about the difference here, but I believe the real
difference is that using
is functional while use
is not really.
I’m actually pretty interested in this. I’ve felt for a long time that timely destructions (aka RAII) is important and weirdly missing in some newer languages. This is pretty encouraging that it’s not as rare as I thought. On the other hand much more of the onus is put on the user, which is unfortunate, but a compromise I think is probably worth making.
Posted Thu, Mar 20, 2014If you're interested in being notified when new posts are published, you can subscribe here; you'll get an email once a week at the most.