Scopes and Finalizers — Guaranteed Cleanup
A Scope is a region of execution with a finalizer registry. Any cleanup effects registered in the scope run when the scope exits — regardless of how it exits.
Creating a Scope
#![allow(unused)] fn main() { use id_effect::{Scope, Finalizer, scoped}; let result = scoped(|scope| { effect! { let conn = ~ open_connection(); // Register cleanup — runs when scope exits ~ scope.add_finalizer(Finalizer::new(move || { conn.close() })); // Do work let data = ~ fetch_data(&conn); process(data) } }); }
scoped creates a scope, runs the inner effect, and then — in all cases — runs the registered finalizers in reverse order.
Finalizers Always Run
#![allow(unused)] fn main() { let result = scoped(|scope| { effect! { let conn = ~ open_connection(); ~ scope.add_finalizer(Finalizer::new(move || conn.close())); ~ risky_operation(); // may panic or fail "done" } }); // Whether risky_operation succeeds, fails, or panics: // conn.close() ALWAYS runs before result is returned }
This is the guarantee that RAII can't provide in async: the finalizer is an async effect that runs in the right context, at the right time, always.
Multiple Finalizers
Finalizers run in reverse registration order (last-in, first-out — like RAII destructors):
#![allow(unused)] fn main() { scoped(|scope| { effect! { let conn = ~ open_connection(); let txn = ~ begin_transaction(&conn); let cursor = ~ open_cursor(&txn); ~ scope.add_finalizer(Finalizer::new(move || close_cursor(cursor))); // runs 3rd ~ scope.add_finalizer(Finalizer::new(move || rollback_or_commit(txn))); // runs 2nd... wait ~ scope.add_finalizer(Finalizer::new(move || close_connection(conn))); // wait — read below } }) }
Actually, the first registered finalizer runs last. Register cleanup in the order you want it to run, reversed: register connection first (so it closes last), cursor last (so it closes first).
Scope Inheritance
Scopes nest. A child scope's finalizers run before the parent's:
#![allow(unused)] fn main() { scoped(|outer| { scoped(|inner| { effect! { ~ inner.add_finalizer(Finalizer::new(|| cleanup_inner())); ~ outer.add_finalizer(Finalizer::new(|| cleanup_outer())); work() } }) }) // Execution order: cleanup_inner(), then cleanup_outer() }
Layers use scopes internally — every resource a Layer builds can register its own finalizer, and the whole graph tears down cleanly when the application shuts down.