Skip to content

Composition and Custom Decorators

Multiple decorators can be stacked before the same function or struct. The application order is bottom-up: the decorator closest to the target is applied first. The example below combines cache, tracing, and retry in a single function, and also shows a struct with @derive + @serialize:

@retry + @log + @memoize on a function; @serialize + @derive on a struct; @log + @benchmark together.

12-stacked.zolo
Playground
// 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

The @const qualifier on a parameter requires the corresponding argument to be a compile-time constant — a literal, a reference to a const, or an arithmetic expression over comptime values. This enables validations (via const_assert) that run at compile time, not at runtime:

Literal, const reference, and comptime arithmetic are accepted; a runtime variable is rejected.

15-const-param.zolo
Playground
// 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 lets you create decorators in pure Zolo. Inside the body, super() calls the target function and returns its value. The mixin decides what to do with that value: forward it, transform it, ignore it (short-circuit), or call it multiple times. Mixins also accept parameters and work on impl methods:

Around, transformation, bottom-up composition, short-circuit, parameter, and mixin on a method.

16-mixin.zolo
Playground
// 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

Challenge

Create a @mixin called clamped(min: int, max: int) that restricts the function's return value to the range [min, max]. Apply it to a function that can return values outside that range and confirm the result.

enespt-br