Guide 32 of 53
Handlers and resource scopes
Lexical handlers, typed overrides and deterministic resource cleanup.
On this page
Lexical handlers, typed overrides and deterministic resource cleanup.
Prefix lexical handlers¶
with handler_value { body } evaluates and installs the handler before the body. It uses the same semantics as handle body with handler_value. Nested scopes restore the enclosing handler, and returns and cleanup retain their lexical boundaries. The value must be a proven Handler.
Practice in By Example · Source program
// Prefix handler installation keeps the interpretation next to its scope.
// expected:
// memory
// inner
// memory
effect Config {
fn read() -> str
}
fn load() with Config -> str = perform Config::read()
let memory = handler {
Config::read() => "memory",
}
let inner = handler {
Config::read() => "inner",
}
print(with memory { load() })
let nested = with memory {
let answer = with inner { load() }
answer
}
print(nested)
print(with memory { load() })Visible resource scopes¶
using resource = acquisition { body } installs cleanup after a successful acquisition. The type must implement core::cleanup::Cleanup. Cleanup follows lexical exits and owned transfers disarm it. A method merely named close does not establish the protocol.
Practice in By Example · Source program
// Resource scopes install cleanup after successful acquisition.
var acquired = 0
struct Resource { id: int }
fn acquire(id: int) -> Result<Resource, str> {
acquired += 1
if id < 0 { .Err("acquisition") } else { .Ok(Resource { id }) }
}
impl core::cleanup::Cleanup for Resource {
fn close(self) { print("close"); print(self.id) }
}
fn read(id: int) -> Result<int, str> {
let value = using file = acquire(id)? {
defer print("body-defer")
file.id + 10
}
Result::Ok(value)
}
print(read(1).unwrap())
print(read(-1).is_err())
print(acquired)
fn early() -> int {
using file = Resource { id: 2 } { return 22 }
0
}
print(early())
fn fail() -> Result<int, str> { .Err("body") }
fn propagated() -> Result<int, str> {
let value = using file = Resource { id: 3 } { fail()? }
Result::Ok(value)
}
print(propagated().is_err())
var count = 0
:outer loop {
count += 1
using file = Resource { id: count + 3 } {
defer print("loop-defer")
if count == 1 { continue :outer }
break :outer
}
}
let tail = using outer = Resource { id: 6 } {
let inner = using inner = Resource { id: 7 } { inner.id }
inner + outer.id
}
print(tail)
@linear struct Owned { id: int }
impl core::cleanup::Cleanup for Owned {
fn close(self) { print("owned-close"); print(self.id) }
}
impl Owned {
fn finish(sink self) { self.close(); let Owned { .. } = self }
}
using owned = Owned { id: 8 } { 0 }
using owned = Owned { id: 9 } {
var __zolo_live_0 = true
owned.finish()
assert(__zolo_live_0)
0
}
fn consume(sink owned: Owned) { owned.close(); let Owned { .. } = owned }
fn seven() -> int { 7 }
fn consume_with(sink owned: Owned, number: int) { assert(number == 7); consume(owned) }
using owned = Owned { id: 10 } {
let __zolo_arg_0 = 13
consume_with(owned, seven())
assert(__zolo_arg_0 == 13)
0
}
let transferred = using owned = Owned { id: 11 } { owned }
consume(transferred)
// The implicit cleanup owns the original binding even when the body shadows it.
let shadow = using file = Resource { id: 12 } {
let file = Resource { id: 13 }
file.id
}
print(shadow)
// Generic sink transfer hands the resource to the caller and disarms cleanup.
fn moved<T: core::cleanup::Cleanup>(sink value: T) -> T {
using resource = value { resource }
}
consume(moved(Owned { id: 14 }))
macro scoped(value) { using resource = $value { consume(resource); 7 } }
print(scoped!(Owned { id: 15 }))
print(scoped!(Owned { id: 16 }))
// An owned generic factory temporary keeps its cleanup obligation.
fn acquire_owned() -> Owned { Owned { id: 17 } }
fn scoped_factory<T: core::cleanup::Cleanup>(factory: fn() -> T) -> int {
let acquired = factory()
using resource = acquired { 7 }
}
print(scoped_factory(acquire_owned))Typed handler overrides¶
A handler with operation selectors replaces only those operations, keeping the remaining effect coverage and answer type. Selector signatures and bodies are checked against the actual effect. Composition preserves the handler frame and lexical abort semantics.
Practice in By Example · Source program
// Composition preserves base evaluation, closures and lexical cleanup.
effect Clock { fn now() -> int; fn zone() -> str }
effect Store { fn put(value: int) -> int }
var calls = 0
var builds = 0
var events = ""
let fixed_time = 42
let __zolo_frame_id_0 = 0
let __zolo_handler_override_compose_0 = 0
let real_world: handler<Clock + Store, int> = handler {
Clock::now() => resume(1),
Clock::zone() => resume("UTC"),
Store::put(value) => resume(value + 1),
}
fn choose(value: handler<Clock + Store, int>) -> handler<Clock + Store, int> { value }
// Forwarding an opaque handler parameter does not establish composition origin.
let test_world = ({ builds += 1; real_world }) with {
Clock::now() => { calls += 1; defer { events += "defer/" }; fixed_time + __zolo_frame_id_0 + __zolo_handler_override_compose_0 },
}
print(with test_world { perform Clock::now() + perform Store::put(2) })
print(with test_world { perform Clock::zone(); 7 })
print(with test_world { perform Clock::now() })
print(calls)
print(events)
print(builds)
let twice = test_world with { Store::put(value: int) => value + 10 }
print(with twice { perform Store::put(2) })
fn lexical(value: handler<Clock + Store, int>) -> int {
defer { events += "caller/" }
with value { return 9 }
0
}
print(lexical(twice))
print(events)Several resources in one scope¶
Separate acquisitions with commas in a using header. Acquisitions run from left to right and later acquisitions can use earlier bindings. Cleanup runs in reverse order. If an acquisition fails, every resource already acquired is cleaned up.
// Each successful acquisition installs cleanup before the next one starts.
struct File { name: str }
fn open(name: str) -> File { File { name } }
impl core::cleanup::Cleanup for File {
fn close(self) { print("close {self.name}") }
}
let copied = using source = open("source"), target = open("target") {
print("{source.name} -> {target.name}")
3
}
// expect: source -> target
// expect: close target
// expect: close source
print(copied) // expect: 3Continue learning¶
DOCS / FEEDBACK
Did this page leave a question?
Tell us where the explanation lost you. Documentation is part of the language experience.