A defer discussion

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.)

4 Likes