Composição e Decorators Próprios
Nesta página
Vários decorators podem ser empilhados antes de uma mesma função ou struct. A
ordem de aplicação é de baixo para cima: o decorator mais próximo do alvo é
aplicado primeiro. O exemplo abaixo combina cache, rastreamento e retry em uma
única função, e também mostra um struct com @derive + @serialize:
@retry + @log + @memoize em função; @serialize + @derive em struct; @log + @benchmark juntos.
// Feature: Stacked decorators — composition
// Syntax: several `@a` `@b` lines before the `fn`/`struct`. Application
// order is bottom to top (the one closest to the target applies first).
// When to use: combine capabilities without entangling logic in the body.
// Case 1: cache + tracing + retry — a "good citizen" function.
@retry(3)
@log
@memoize
fn lookup_user(id: int) -> str {
// In production: a DB/HTTP call. Here, a deterministic stub.
return "user-{id}"
}
print(lookup_user(1))
print(lookup_user(1)) // memoize avoids re-execution
print(lookup_user(2))
// expected:
// user-1
// user-1
// user-2
// Case 2: struct with Debug + Clone + Eq + serialize.
@serialize
@derive(Debug, Clone, Eq)
struct Item {
sku: str,
price: float,
}
let a = Item { sku: "X-1", price: 9.9 }
let b = a.clone()
print(a)
// expected: Item { sku: "X-1", price: 9.9 }
print("a == b? {a == b}")
// expected: a == b? true
print(a.to_json())
// expected: {"sku":"X-1","price":9.9}
// Case 3: log + benchmark — instrument and measure together.
@log
@benchmark
fn hot_path(n: int) -> int {
return n * n + n
}
print("hot(10) = {hot_path(10)}")
// expected: hot(10) = 110
O qualificador @const em um parâmetro exige que o argumento correspondente
seja uma constante em tempo de compilação — literal, referência a const ou
expressão aritmética sobre valores comptime. Isso habilita validações
(via const_assert) que rodam na compilação, não no runtime:
Literal, referência a const e aritmética comptime são aceitos; variável runtime é rejeitada.
// Feature: `@const` parameter qualifier
// Syntax: `fn name(@const NAME: TYPE, ...)`
// When to use: function runs at runtime, but a specific argument must
// be a comptime-constant — useful for fixed-size buffers, format
// string validation, SQL DSL validation, bit-field accessors, and
// any API where the argument enables compile-time checks via
// `const_assert`.
//
// See `specs/const-parameter.md` for the full specification.
// 1. Basic shape ----------------------------------------------------
fn ring_buffer(@const capacity: int, name: str) -> int {
// (Inside the body the parameter behaves like a runtime value;
// comptime-conscious validation runs at the call site.)
return capacity
}
// 2. Accepted call shapes ------------------------------------------
let r1 = ring_buffer(64, "audio") // literal — OK
print("r1 = {r1}")
const DEFAULT_CAP = 128
let r2 = ring_buffer(DEFAULT_CAP, "video") // const reference — OK
print("r2 = {r2}")
let r3 = ring_buffer(32 + 16, "control") // arithmetic over comptime values — OK
print("r3 = {r3}")
// 3. Rejected call shape -------------------------------------------
// Uncomment to trigger the type error:
//
// let n = read_int()
// let bad = ring_buffer(n, "dynamic")
// // ^ E_ConstArgNotComptime: argument 1 must be a
// // comptime-constant expression (parameter is
// // declared `@const`)
//
// Bypass options:
// - hoist the value into a `const`:
// const N = 256
// ring_buffer(N, "fixed")
// - inline the literal directly:
// ring_buffer(256, "fixed")
// 4. Why @const is useful ------------------------------------------
// Inside `ring_buffer`, the `capacity` parameter can be referenced
// in a `const_assert` to enforce invariants at every call site:
//
// fn ring_buffer(@const capacity: int) {
// const_assert capacity > 0
// const_assert capacity.is_power_of_two()
// ...
// }
//
// (Comptime evaluation of `@const` params in `const_assert` is a
// follow-up — the parameter is comptime-known at the call site, but
// the check passes through runtime today. Track in
// `specs/const-parameter.md` §Interactions.)
@mixin permite criar decorators em Zolo puro. Dentro do corpo, super() chama
a função alvo e retorna o valor dela. O mixin decide o que fazer com esse valor:
repassar, transformar, ignorar (short-circuit) ou chamar múltiplas vezes. Mixins
também aceitam parâmetros e funcionam em métodos de impl:
Around, transformação, composição bottom-up, short-circuit, parâmetro e mixin em método.
// Feature: Mixin functions — user-defined decorators written in Zolo.
//
// A `@mixin fn` wraps a target function. Inside the body, `super()` calls the
// wrapped target. You apply a mixin like any decorator: `@name`. Unlike the
// built-in decorators (@memoize, @retry, ...), mixins live in userland — the
// compiler only knows that a mixin is "a fn whose body may call super()".
//
// Mixins compose bottom-up, exactly like stacked decorators: the one closest
// to the `fn` is the innermost layer.
// ── 1. Around: run code before and after the target ──────────────────
// `super()` returns whatever the target returns; the mixin's own return
// value is what the caller sees (here we just pass it through).
@mixin
fn traced() -> int {
print(">> enter")
let r = super()
print("<< exit")
return r
}
@traced
fn square(n: int) -> int {
return n * n
}
print(square(5))
// expected:
// >> enter
// << exit
// 25
// ── 2. Transform the result ──────────────────────────────────────────
// The mixin can do anything with the wrapped call's value.
@mixin
fn doubled() -> int {
return super() * 2
}
@doubled
fn ten() -> int {
return 10
}
print(ten())
// expected: 20
// ── 3. Composition (stacking) ────────────────────────────────────────
// Bottom-up: `plus_one` is innermost (10 + 1 = 11), `times_three` wraps it
// (11 * 3 = 33). Reversing the two lines would yield (10 * 3) + 1 = 31.
@mixin
fn plus_one() -> int {
return super() + 1
}
@mixin
fn times_three() -> int {
return super() * 3
}
@times_three
@plus_one
fn base() -> int {
return 10
}
print(base())
// expected: 33
// ── 4. Short-circuit: a mixin may choose NOT to call super() ──────────
// Useful for feature flags, circuit breakers, dry-runs. The target never
// runs, so its body is skipped entirely.
@mixin
fn disabled() -> int {
print("blocked")
return 0
}
@disabled
fn dangerous() -> int {
print("this should never run")
return 999
}
print(dangerous())
// expected:
// blocked
// 0
// ── 5. Parameterised mixins ──────────────────────────────────────────
// Declare parameters on `@mixin(...)`; pass values at the application site.
// This is the shape of @retry(n), @cache(ttl), @rate_limit(rps), etc. The
// parameter is an ordinary local inside the body; super() still forwards the
// target's own arguments.
@mixin(factor: int)
fn scaled() -> int {
return super() * factor
}
@scaled(4)
fn six() -> int {
return 6
}
print(six())
// expected: 24
// A parameter can also drive how many times the target runs.
@mixin(times: int)
fn repeated() -> int {
var total = 0
for _ in 0..times {
total = total + super()
}
return total
}
@repeated(3)
fn unit() -> int {
return 1
}
print(unit())
// expected: 3
// ── 6. Mixins on methods ─────────────────────────────────────────────
// super() forwards the receiver automatically. A mixin that reads `self`
// becomes a method mixin and binds the receiver in its body.
@mixin
fn audited() -> int {
print("audit: balance was {self.balance}")
return super()
}
struct Account {
balance: int,
}
impl Account {
@audited
fn read(self) -> int {
return self.balance
}
}
let acct = Account { balance: 42 }
print(acct.read())
// expected:
// audit: balance was 42
// 42
Desafio
Crie um @mixin chamado clamped(min: int, max: int) que restringe o retorno
da função ao intervalo [min, max]. Aplique-o a uma função que pode retornar
valores fora desse intervalo e confirme o resultado.
Veja também