Resource management with on_exit and Scope
on_exit attaches a finalizer that runs when the effect settles, on success and failure alike. A Scope collects finalizers from a whole sub-tree: acquire_and_release registers a release for an acquired resource, and .scoped() provides the Scope and runs the collected finalizers in reverse order when the wrapped effect settles. It composes with provide chains — program.provide(Db)(db).scoped() — and is a no-op on effects that never acquired a Scope, so it can uniformly terminate a chain.
E.(21).(E.("done")) # finalizer runs on success and failure alike
= E.(
E.(lambda: .()), # acquire
lambda : E.(.), # release, guaranteed by the enclosing scope
) # Effect[Connection, Never, Scope]
= .(
).() # Scope discharged; close() runs when program settleson_exit also takes a function that receives the Exit and returns the finalizer, so cleanup can depend on how the effect settled:
E.(21).(
lambda : E.("settled", )
) # Succeeded(21), or Failure(cause) carrying Fail, Die or InterruptA finalizer that dies doesn't skip the remaining finalizers; its defect surfaces in the final Exit.
More examples: test_scope.py.