Why this fires¶
A parameter declared as a plain function type — fn(int) -> int, no with clause — promises purity: whatever runs inside it performs no effects the caller doesn't already know about. Passing a function that requires an effect there lets that effect escape completely undeclared, the same class of leak TE802 blocks in the ordinary call direction.
effect Cookies { fn get(n: str) -> str? }
fn precisa_puro(f: fn(int) -> int) -> int { return f(1) }
fn efetuoso(n: int) with Cookies -> int {
let _ = perform Cookies::get("x")
return n
}
let _ = precisa_puro(efetuoso)
// error[TE815]: argument 1 to 'precisa_puro': function requires the effect
// `Cookies`, but this parameter expects a pure functionefetuoso performs Cookies, but precisa_puro's signature gives the caller no way to know that — nothing downstream declares with Cookies, so the effect would surface with no handler in scope.
Fix it¶
1. Give the parameter an open row¶
If the function is meant to forward whatever effects its argument needs — the row-polymorphic combinator shape used by std::effect's own map_with_effect/parallel — declare a tail and propagate it:
fn combinador<e>(f: fn() with {| e} -> int) with {| e} -> int {
return f()
}A row with a tail is an open contract: the combinator doesn't discharge the effect, it passes it through, so the caller is still on the hook to handle it.
2. Handle the effect before crossing the pure boundary¶
fn efetuoso_puro(n: int) -> int {
handle {
let _ = perform Cookies::get("x")
return n
} with {
Cookies::get(_) => .Some("stub"),
}
}
let _ = precisa_puro(efetuoso_puro)3. For a std::web route handler, use the pipe form¶
http.serve/web.serve are not involved in this check — both take their handler (or router) as Any, so neither one rejects anything based on effects by itself. What does have a plain (pure) handler parameter is http.get/post/put/delete and web.get/post/put/delete — passing an effectful handler as a direct call argument to any of them triggers TE815 the same way:
use std::web
fn home(_req) with Cookies -> str {
let _ = perform Cookies::get("x")
return "hi"
}
let r = web.router()
let r2 = web.get(r, "/", home) // error[TE815]: argument 3 to 'web.get' ...std::web's own examples (e.g. examples/features/36-web/11-cookies.zolo) register handlers with the pipe form instead, which does not hit this check:
let app = web.router() |> web.get("/", home)
web.serve(3099, app)If you need to pass an effectful handler as an ordinary call argument (not through the pipe), discharge its effects first — see method 2 above.
Known limitation¶
This check only protects a target parameter whose row is literally empty — no with clause at all (fn(int) -> int). A target with a closed but non-empty row, like fn() with Fail, is not covered:
effect Fail { fn fail(r: str) -> int }
effect Cookies { fn get(n: str) -> str? }
fn precisa_so_fail(f: fn() with Fail -> int) -> int { return f() }
fn vaza_cookies() with Fail + Cookies -> int {
let _ = perform Cookies::get("x")
return perform Fail::fail("x")
}
let _ = precisa_so_fail(vaza_cookies)
// compiles clean today — `Cookies` leaks past `precisa_so_fail` undeclared.precisa_so_fail only promised Fail, but the argument also performs Cookies, and nothing catches that. This is a real residual soundness gap, not a defect in the current rule — the fix (checking that the source's fixed effects are a subset of the target's, when both rows are closed) is deliberate future work, tracked separately from this slice. If you hit it, that's why.
See also¶
TE802— calling a function that requires an undeclared effect.TE809— a function performs effects but declares none of them.- /docs/algebraic-effects — effect rows, open vs. closed, and row polymorphism.