|
It seems like what is really wanted is a way to clean up a scope when it closes, so it seems like we should just do that. Create a new block frame, such as "defer ... end" or "exiting ... end". Everything that is in scope at the point where the block begins is in scope throughout the block. However, the contents of the block are not executed inline; rather, once execution reaches the point in the code where the block begins, a deferred-execution guarantee is made for the contained code, which will be executed when the scope immediately containing the exiting ... end block is exited (in newest-guarantee-first order). function foo(bar, ...) local baz = bar exiting baz.closed = true end do local quoz = {...} exiting setmetatable(quoz, {__mode='kv'}) end quoz[0] = bar.file or error ("'file' field missing from supplied parameter") local moz = io.open(quoz[0], 'a+') exiting moz:close() end if riz(moz, baz) then -- some global function return true end end print("warning: operation could not complete") return false end So let's break this down a little: After the local baz is assigned from bar, a guarantee is made that baz.closed will be set to true. This will happen even if bar has been changed. Inside the do block quoz is assigned to a new table created from varargs. A guarantee is then made that before execution exits the do block, this table will receive a metatable that makes its references weak. This will happen BEFORE baz.closed is set. A new key is added to this table provided that bar.file is not nil or false (it is probably more typical to use assert here, but this still illustrates the principle). If the error is thrown, quoz will get its metatable, then baz.closed will become true, then the error will propagate out of the function. (However, the code just after this, to call moz:close(), will not be called at all, as execution never reached its deferral.) Provided there is no error, moz is assigned to a newly opened file. A guarantee is then made that this file will be closed before the do block can exit, before the metatable is applied to quoz (this guarantee of reverse execution is something I believe is well-understood to be necessary to keep code maintainable and deterministic). Then, if the riz funciton returns some true value, moz is closed, quoz is set to weak references, baz.closed is set, and execution then leaves foo with the return value true. If riz returns a false value, execution proceeds out of the do block. moz is closed and quoz is metatabled. The print statement is executed. Finally, returning false triggers setting baz.closed to true before execution returns to the calling function. The exiting block is not actually part of the assignment syntax. So local baz = bar exiting baz.closed = true end is the same as local baz = bar; exiting baz.closed = true end which is the same as local baz = bar exiting baz.closed = true end And there is also no reason you can't have an exiting block without any associated variables. |