This example doesn't support your argument, because you've changed the behavior of the program. With do
/final
, the cleanup runs before Use the variables
, and with defer
it runs after. If you had written the same program in both styles, then they would also have identical scoping properties - Use the variables
would have to move either inside the do
block, or outside of the defer
's containing block.
This is because defer
is also tied to a block! It's just tied to its containing scope rather introducing a new block itself. You can always convert a defer
program to a do
/finally
program without changing the scoping structure: wrap the region between the defer
and the end of its block in do
and move the cleanup into final
. There will, by definition, never be any code in the same scope that needs to run after the cleanup.
I do think there is an argument to be made for tying initialization and cleanup together. That's why we have Drop
, after all. But this can be accomplished without the out-of-order nature of defer
. The nesting pattern used by APIs like thread::scope
puts the initialization and cleanup together in a higher-order function, with similar benefits to the way Drop
ties cleanup to the type. (Notably defer
does not quite manage this.)
There are also ways to recover the ability to jump out of an "abstracted" do
/final
block like this. A crude one is to use a macro instead of a higher-order function. Alternatively, the closure could capture its control flow environment, with an implementation along the lines of Kotlin's inline fun
. We could also deem this particular trifecta (initialization+cleanup together, not using Drop
, and early-exit control flow) to be the realm of undroppable types, or not worth supporting at all.
(And, for those who still want to reduce rightward drift, there is syntactic sugar like Koka's with
that captures the tail of a block following a binding, wraps it up in a closure with the binding as a parameter, and passes it to a higher-order function.)