Rationale

It can be difficult to manage resource lifetimes, especially when laziness is involved. Simply local scoping can be inadequate. Resource scopes can decouple resource lifetimes from creating scope.

Plan

Use Cases

Problems

Issues

Proposal

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:

Caller A creates a scope. Lib B combines several resources to do some
work for A, some of which will be lazy/incomplete when returning to
A. The challenge is that B wants to clean up some, but not all, of the
scoped things it used. Some possibilities:

I believe that 95% of the current problems people experience are solved with the simple mechanism proposed here. My preference is to start with "you're on your own" and evolve to "scope grab" if necessary. The latter is an easy, non-breaking extension.

TBD to flesh out this approach

Scenarios

  1. Creating a resource whose lifetime is bounded to yours: use with-open, as you do today
  2. Creating a resource that will be passed back to a caller unconsumed: call (scope res)
    1. caller must have made a scope
    2. mandatory scope is not a limitation here, it is a sensible requirement
  3. Returning >1 resources from a function with different lifecycles and cleanup rules: not supported
    1. not going to worry about this without realistic example
  4. Create resources from child threads
    1. works if thread creation conveys bindings
    2. you are responsible for waiting on child threads
  5. Consuming one resource, pass another through to caller: put with-open around the one you are consuming
    ;; a cleaned up here, b cleaned up by caller
    (let [a (with-open [a ...] ...
          b (scope ...)]
      b)
    
  6. Decide on the fly whether to clean up or let parent do it
    ;; capture-scope? grab-scope? capture-cleanup?
    (let [[a s] (capture-scope (make-a))]
      (if (done? ...)
        (s) ;; cleanup myself
        (scope s) ;; punt
    ))