...
- dynamic binding
*scope*holds collection of things needing cleanup - with-open binds a new
*scope*, cleans up*scope*at end - lib APIs can call a new fn
(scope thing)to add a resource to the current bound scope- throws exception if no scope available
- REPL cleans up scope before every loop
- *scope* is an atom, so N "child" threads can add to it
- binding conveyance makes this work
- still up to you to make sure "parent" thread outlives children
IMO this nails all the use cases and problems above, with a few
caveats that I think can be subsumed into a single example as follows:
...
- investigate interaction with fork/join
- think this reduces to "make binding conveyance work with fork/join"
- new protocol or IFn for bits of cleanup logic placed in a scope
- handling Closeable and IFn in an if statement for now
- code in core happens before protocols are available
- verify that change to with-open is non-breaking for existing code
- but note no benefit either, until you start calling (scope ...)
Scenarios
- Creating a resource whose lifetime is bounded to yours: use with-open, as you do today
- Creating a resource that will be passed back to a caller unconsumed: call (scope res)
- caller must have made a scope
- mandatory scope is not a limitation here, it is a sensible requirement
- Returning >1 resources from a function with different lifecycles and cleanup rules: not supported
- not going to worry about this without realistic example
- Create resources from child threads
- works if thread creation conveys bindings
- you are responsible for waiting on child threads
- Consuming one resource, pass another through to caller: put with-open around the one you are consuming
Code Block ;; a cleaned up here, b cleaned up by caller (let [a (with-open [a ...] ... b (scope ...)] b) - Decide on the fly whether to clean up or let parent do it
Code Block ;; capture-scope? grab-scope? capture-cleanup? (let [[a s] (capture-scope (make-a))] (if (done? ...) (s) ;; cleanup myself (scope s) ;; punt ))