Tutorial — освобождение ресурсов через consume{} (Plan 110)
Plan 110.8.2. Глава туториала, знакомящая с scope-блоком
consume X = ... { body }для освобождения ресурсов.
Почему важно освобождение ресурсов
При работе с ресурсами (файлы, соединения с БД, блокировки) нужно гарантировать, что они всегда освобождаются, даже при ошибках. Забытое освобождение приводит к:
- Утечкам ресурсов — заблокированные файлы, зависшие соединения с БД.
- Взаимоблокировкам — Mutex удерживается во время паники, остальные ждут вечно.
- Повреждению данных — транзакция не откачена, наполовину записанное состояние.
Nova предоставляет scope-блок consume X = expr { body }, чтобы освобождение
было автоматическим и надёжным.
Первый пример — чтение файла
fn read_config(path str) Fail[IoError] -> Config {
consume f = File.open(path)? {
ro raw = f.read_all()!!
return Config.parse(raw)!!
}
// f.@cleanup (File.close) automatically called here.
}
Что происходит:
File.open(path)?открывает файл. Если это не удалось,?пробрасывает ошибку —fникогда не связан, освобождение не нужно.fдоступен внутри тела{ ... }.- После завершения тела (успех ИЛИ ошибка)
f.@cleanup(outcome)вызывается автоматически — закрывает файл. - Если тело упало, ошибка пробрасывается дальше ПОСЛЕ освобождения.
Почему
!!, а не?внутри тела. Оба оператора разворачивают, но пробрасывают по-разному (D85):?— return-стиль и законен только в функции, возвращающейResult/Option; в функции, объявившейFail[E], этоE_TRY_IN_FAIL_FN.!!— throw-стиль: бросает через эффектFail, который эти функции как раз и объявляют.?на строке инициализатора остаётся и означает другое:consume X = expr? { body }— форма unwrap-инициализации D196.Почему
return, а не просто хвостовое выражение. Биндинг-формаconsume X = expr { body }— STATEMENT, её значение unit (разворачивание D188). Выражением объявлена только re-consume формаconsume X { body }.returnизнутри тела очистку не пропускает —@cleanupотрабатывает раньше.
Протокол Cleanup[E]
Любой тип можно использовать в consume X = ... { body }, реализовав протокол
Cleanup[E]:
type Cleanup[E] protocol {
@cleanup(outcome ScopeOutcome) Fail[E] -> ()
}
type ScopeOutcome
| Success
| Failure(str)
| Panic(str)
E— тип ошибок, которые может бросить сам@cleanup(например,IoError, если close может упасть).Success— тело завершилось нормально.Failure(msg)— тело бросило ошибку (включая отмену).Panic(msg)— тело запаниковало (программный баг).
Реализация освобождения для своего типа
Пример: транзакция БД
type Transaction { conn DbConn, id int }
fn Transaction consume @cleanup(outcome ScopeOutcome) Fail[DbError] -> () {
match outcome {
Success => @conn.commit(@id)!!
Failure(_) => @conn.rollback(@id)!!
Panic(_) => @conn.rollback_emergency()
}
}
Использование:
fn process_order(db Db, order Order) Fail[DbError] -> () {
consume tx = db.begin() {
db.insert_order(order)!!
db.notify_warehouse(order.id)!!
}
// Success → commit; failure → rollback (automatic).
}
Пример: безошибочное освобождение (Mutex-лок)
Для ресурсов, где освобождение НЕ МОЖЕТ упасть, используйте Cleanup[never]:
fn MutexGuard consume @cleanup(_outcome ScopeOutcome) -> () => @unlock()
// ^^^^ no Fail[E]
Вызывающему не нужен Fail[E]:
fn increment_counter(state State) -> () { // no Fail!
consume _l = state.mutex.lock() { // Cleanup[never]
state.value += 1
}
}
Различение исхода
Тело @cleanup может ветвиться по исходу:
fn HttpRequest consume @cleanup(outcome ScopeOutcome) -> () {
match outcome {
Success => @metrics.inc("http.success")
Failure(msg) => {
if msg.starts_with("cancel: ") {
@metrics.inc("http.cancel")
} else {
@metrics.inc("http.error")
}
}
Panic(_) => @metrics.inc("http.panic")
}
@release_pool_slot()
}
Вложенные области
Области вкладываются естественно — внутренняя выходит раньше внешней (LIFO):
fn deep_work(addr str) Fail[NetError] -> () {
consume conn = pool.acquire()? {
consume tx = conn.begin()? {
consume stmt = tx.prepare(sql)? {
stmt.execute(args)!!
}
// stmt.@cleanup fires first.
}
// tx.@cleanup fires (commit or rollback).
}
// conn.@cleanup fires last (release to pool).
}
Смесь consume{} + defer
Оба работают вместе. defer срабатывает внутри своей области ДО @cleanup:
fn process() -> int {
mut counter = 0
consume r = Resource.new() {
defer { counter += 100 } // fires when body ends
counter += r.id
}
// Order: defer body (counter += 100) → r.@cleanup
counter
}
Формы инициализации (D196)
Выражение инициализации поддерживает:
// Direct method call
consume tx = db.begin() { ... }
// Result unwrap via ?
consume tx = db.try_begin()? { ... } // Result[Tx, DbError] → Tx
// Option unwrap via !!
consume tx = maybe_tx()!! { ... } // Option[Tx] → Tx
// Conditional (both branches same type)
consume r = if local { LocalRes.new() } else { LocalRes.connect()? } { ... }
Формы, которые не работают:
// ❌ Option without unwrap
consume tx = maybe_tx() { ... }
// → D196-wrapped-init-needs-unwrap
// ❌ Different types in branches
consume r = if cond { ResA.new() } else { ResB.new() } { ... }
// → D196-divergent-consumable
Сравнение с другими языками
// Rust — implicit, no syntax marker
let f = File::open(path)?;
f.read_all()
// Drop fires automatically
# Python with statement
with open(path) as f:
f.read()
// Nova consume{}
consume f = File.open(path)? {
f.read_all()?
}
Преимущества Nova:
- Видимость: освобождение явное, а не магический
Drop. - Cancel-shield: освобождение защищено от шторма отмен (D188 R3).
- Осведомлённость об исходе: ресурс различает success/failure/panic.
- Async-совместимость: можно
awaitвнутри@cleanup(D191).
Что дальше
- Прочитай Q-cleanup-semantics для деревьев решений.
- Прочитай Q-consumable-protocol для деталей реализации.
- Прочитай Q-application-effect для жизненного цикла приложения.
- Прочитай cleanup-cookbook.md для производственных рецептов.
См. также
- D188 — Cleanup + scope-block.
- Plan 110.
- Все Q-блоки в docs/dev/idioms/.