Pular para o conteúdo

Tipos Avançados

Nesta página

Operações multi-shot

A palavra-chave multi marca uma operação cujo handler pode invocar resume(v) mais de uma vez, explorando múltiplas continuações — não-determinismo, busca, geradores. No Marco A o compilador já valida os tipos; a semântica de runtime chega no Marco B. Declarar multi agora garante que o contrato estará correto quando o runtime evoluir:

multi fn pick(opts: [int]) declarado; handler one-shot determinístico funciona hoje; preview do Marco B comentado.

11-multi-shot.zolo
Playground
// Feature: `multi fn op(...)` — declares a multi-shot operation. The

// `multi` keyword marks ops whose handler arms may invoke `resume(v)`

// more than once, exploring multiple continuations of the same

// computation (non-determinism, search, generators).

//

// Marco A: the keyword is recognised by the parser and threaded through

// the type system. The VM backend treats `multi` ops identically to

// one-shot at runtime today — a handler that calls `resume` more than

// once is undefined behaviour. The native backend rejects any `perform`

// of a `multi` op (compile error TE812) because stack-copying

// continuations are not yet implemented.

//

// When to use: declare `multi` whenever the operation could *logically*

// support multi-shot — so the contract is correct ahead of the Marco B

// runtime work. Marco B will turn the annotation into real semantics

// without any change to the source below.


effect Choice {
  multi fn pick(opts: [int]) -> int
}

fn coin_flip() with Choice -> str {
  let n = perform Choice::pick([0, 1])
  if n == 0 { return "heads" }
  return "tails"
}

// One-shot handler: today, arms call `resume(v)` at most once. The

// `handle` block evaluates to whatever the body produces after resume.

let trial = handle coin_flip() with {
  Choice::pick(opts) => {
    // Deterministic stand-in: always pick the first option.

    resume(opts[0])
  },
}
print("trial: {trial}")
// expected: trial: heads


// Type-check enforcement (Marco A): args to `perform` are checked

// against the operation's declared parameter types. The following line,

// if uncommented, would fail with TE808 — `[true, false]` is `[bool]`,

// not `[int]`:

//

//   let bad = perform Choice::pick([true, false])

//

// Likewise, handler arm bodies are checked against the op's return

// type. A body that produces `str` for a `pick` arm would also fail

// (`pick` returns `int`).


// Marco B preview — the same source will gain real multi-shot semantics

// once the VM grows stack-copying continuations. The handler below is

// the canonical "explore every branch" shape; it does not run today,

// but the declaration above is already correct for it:

//

//   handle coin_flip() with {

//     Choice::pick(opts) => {

//       let mut results = []

//       for o in opts { results.push(resume(o)) }

//       results

//     },

//   }

//   // Marco B output: ["heads", "tails"]

Efeitos genéricos

Um efeito pode ser parametrizado por um tipo: effect State<T> funciona igualmente para State<int> e State<str>. O compilador resolve T no ponto de instalação via unificação com as cláusulas do handler — e recusa misturar handlers de tipos diferentes:

State<int> para contar itens; State<str> para acumular nomes; mesma declaração, dois handlers distintos.

17-generic-state.zolo
Playground
// Feature: generic effects — `effect State<T> { ... }` declares a

// type-parameterised effect. The same source supports `State<int>`,

// `State<str>`, or any other monomorphisation; the compiler picks `T`

// at the install site via unification with arm bodies or with the

// surrounding context (`with State<int>` on the fn signature).

// Syntax: `effect Name<T1, T2, ...> { fn op(p: T1) -> T2 }` declares;

// `with State<int>` pins T at perform sites.

// When to use: any state-shaped capability you want to apply to more

// than one concrete value type without duplicating the effect.


effect State<T> {
  fn get() -> T
  fn set(v: T)
}

// `with State<int>` pins T = int for the body's perform sites.

fn count(items: [str]) with State<int> -> int {
  perform State::set(0)
  for _ in items {
    perform State::set(perform State::get() + 1)
  }
  return perform State::get()
}

// The handler closes over a mutable int. Both arms type-check against

// State<int>: `get`'s body returns `n: int` and `set`'s param `v: int`

// unifies T = int.

let n = 0
let int_handler = handler {
  State::get() => n,
  State::set(v) => { n = v },
}

let total = handle count(["a", "b", "c", "d", "e"]) with int_handler
print("counted: {total}")
// expected: counted: 5


// Same effect, different T. A second handler for `State<str>`.

fn collect_names() with State<str> -> str {
  perform State::set("")
  perform State::set("alice")
  perform State::set("bob")
  return perform State::get()
}

let s = ""
let str_handler = handler {
  State::get() => s,
  State::set(v) => { s = v },
}

let last = handle collect_names() with str_handler
print("last set: {last}")
// expected: last set: bob


// The two handlers are different `handler<State<int>, int>` and

// `handler<State<str>, str>`. The compiler refuses to mix them — a

// `handle count(...) with str_handler` would be a type error at the

// install site.

Polimorfismo de linha

Combinadores como retry precisam envolver uma computação que já carrega seus próprios efeitos. A variável de linha e na assinatura representa "quaisquer efeitos extras que o chamador traz" — eles passam através do combinador sem serem nomeados:

fn retry<T, e>(n: int, f: fn() with {Fail | e} -> T) with {Fail | e} -> T

O chamador declara apenas os efeitos que ele mesmo trata; o combinador cuida de Fail:

attempt traz Fail + Log; caller declara apenas Log; effect.retry consome Fail e passa Log adiante.

18-row-polymorphism.zolo
Playground
// Feature: row polymorphism — write a combinator once, use it under

// any caller's effect set. The row variable `e` represents "any extra

// effects the user brings"; the combinator passes them through

// unchanged.

// Syntax: `fn name<T, e>(...) with {Fail | e}` declares `e` in the

// generic param list and uses it as a row tail. At call sites, `e`

// binds to whatever extra effects the argument's signature requires.

// When to use: any helper that wraps an effectful computation —

// retry, timeout, parallel, map_with_effect — without locking the

// inner computation's effect set.


use std::effect

// User declares Fail + Log. `Fail` matches the stdlib's declaration

// by name; effects are nominal across the program.

effect Fail { fn fail(reason: str) -> int }
effect Log { fn info(msg: str) }

// `attempt` brings BOTH Fail (which retry expects) and Log (which the

// caller must handle via the row var `e`).

fn attempt() with Fail + Log -> int {
  perform Log::info("trying...")
  return 42
}

// `caller` declares only Log — Fail is consumed by retry's signature.

// The row variable in retry's signature carries Log through unchanged,

// so the substitution at the retry call site yields `with Log`.

fn caller() with Log -> int {
  return effect.retry(3, attempt)
}

let answer = handle caller() with {
  Log::info(m) => print("[log] {m}"),
}
print("got: {answer}")
// expected:

//   [log] trying...

//   got: 42

Aliases de efeito

type FullStack = Db + Fs + Log abrevia conjuntos de efeitos repetidos. O alias expande de forma transparente em todo ponto with — o sistema de tipos trata with FullStack como with Db + Fs + Log em todos os lugares:

type FullStack = Db + Fs + Log; assinatura e handler usam o alias sem expandir manualmente.

19-effect-aliases.zolo
Playground
// Feature: effect aliases — `type Name = E1 + E2 + ...` lets you

// abbreviate common effect bundles. Aliases expand transparently at

// every `with` site; the type system treats `with FullStack` as

// `with Db + Fs + Log` everywhere.


effect Db {
  fn query(sql: str) -> str
}

effect Fs {
  fn read(p: str) -> str
}

effect Log {
  fn info(m: str)
}

// One declaration captures the common dependency set for the whole

// app's data layer.

type FullStack = Db + Fs + Log

fn workflow() with FullStack -> str {
  perform Log::info("starting")
  let data = perform Fs::read("config.toml")
  let row = perform Db::query("SELECT * FROM users")
  return "data: {data}, row: {row}"
}

let result = handle workflow() with {
  Db::query(_sql) => "user:42",
  Fs::read(p) => "[stub: {p}]",
  Log::info(m) => print("[log] {m}"),
}

print(result)
// expected:

//   [log] starting

//   data: [stub: config.toml], row: user:42

Suite de combinadores std::effect

std::effect fornece retry, parallel e map_with_effect já prontos. Todos são polimórficos de linha — funcionam independentemente dos efeitos extras que as funções internas declarem:

effect.retry com função frágil; effect.parallel com duas tarefas; effect.map_with_effect sobre lista.

20-combinator-suite.zolo
Playground
// Feature: row-polymorphic combinator suite — `retry`, `parallel`,

// `map_with_effect` from `std::effect`. Each carries the caller's

// effects through the row variable, so the same combinator works

// regardless of what extra effects the inner fns perform.


use std::effect

effect Fail {
  fn fail(reason: str) -> int
}

effect Log {
  fn info(m: str)
}

// ── retry ──────────────────────────────────────────────────────────

var attempts = 0

fn flaky() with Fail + Log -> int {
  attempts = attempts + 1
  perform Log::info("attempt {attempts}")
  if attempts < 3 {
    perform Fail::fail("not yet")
  }

  return 42
}

fn run_retry() with Fail + Log -> int {
  return effect.retry(5, flaky)
}

let r = handle run_retry() with {
  Fail::fail(_r) => -1,
  Log::info(m) => print("[log] {m}"),
}

print("retry result: {r}")

// expected:

//   [log] attempt 1

//   [log] attempt 2

//   [log] attempt 3

//   retry result: 42


// ── parallel ───────────────────────────────────────────────────────

fn task(n: int) with Log -> int {
  perform Log::info("task({n})")
  return n * 10
}

fn task1() with Log -> int { return task(1) }

fn task2() with Log -> int { return task(2) }

fn run_parallel() with Log -> [int] {
  return effect.parallel([task1, task2])
}

let pa = handle run_parallel() with {
  Log::info(m) => print("[par] {m}"),
}
print("parallel: {pa}")

// expected:

//   [par] task(1)

//   [par] task(2)

//   parallel: [10, 20]


// ── map_with_effect ────────────────────────────────────────────────

fn run_map() with Log -> [int] {
  return effect.map_with_effect([10, 20, 30], task)
}

let ma = handle run_map() with {
  Log::info(m) => print("[map] {m}"),
}
print("map: {ma}")
// expected:

//   [map] task(10)

//   [map] task(20)

//   [map] task(30)

//   map: [100, 200, 300]

Buscar no Zolo

9 resultados

enespt-br