Uniões marcadas, estilo Zero
choice declara um tipo cujo valor é uma de várias variantes nomeadas, cada uma carregando o próprio payload:
choice Result {
ok: i32,
err: String,
}
Um valor Result é ou um ok carregando um i32 ou um err carregando uma String. Nunca os dois, nunca nenhum. O sistema de tipos faz essa garantia, e match torna conveniente agir em cima.
É a mesma ideia que outras linguagens chamam de "tagged union", "sum type", "discriminated union" ou "algebraic data type". Zero chama de choice e mantém a gramática pequena.
Declarando um choice
choice Nome {
varianteA: TipoDePayloadA,
varianteB: TipoDePayloadB,
}
Cada linha lista uma variante. O nome é seu; o tipo depois dos dois pontos é o payload que aquela variante carrega. Uma variante que não precisa de payload usa Void:
choice Token {
word: String,
number: i32,
eof: Void,
}
Token.eof é uma variante sem payload útil (o tipo do payload é Void) — útil para casos do tipo terminador.
Construindo um valor de choice
Crie um valor nomeando o tipo, depois a variante, depois passando o payload:
let success = Result.ok(42)
let failure = Result.err("validation failed")
O tipo do payload precisa bater com o declarado para a variante. Result.ok("hello") seria erro de compilação porque ok espera um i32.
Inferência de tipo funciona aqui também. Se o lado direito determinar completamente o tipo, você pode escrever let success = Result.ok(42) e o tipo da ligação é Result. Anotar o tipo explicitamente está OK quando você quer documentar no ponto da ligação:
let success: Result = Result.ok(42)
Casando um choice
match é como você lê um valor choice. A forma:
match valor {
.varianteA => ligação { /* corpo quando valor é varianteA, com payload em `ligação` */ }
.varianteB => ligação { /* corpo quando valor é varianteB */ }
}
Um exemplo trabalhado do repositório oficial do Zero — clique em Run para ver o braço .ok disparar:
Leia o match literalmente: "dependendo de qual variante result carrega, execute o braço correspondente e vincule o payload ao nome escolhido". No braço .ok, value é o payload i32. No braço .err, message é o payload String. Cada braço é um escopo separado; a ligação só é visível dentro do próprio corpo.
Exaustividade
Esse é o grande ganho do match sobre cadeias if/else if: o compilador verifica que toda variante tem um braço. Se você esquecer o caso .err, não tem fallthrough em runtime para um default — você recebe um erro de compilação:
{
"code": "MAT001",
"message": "match is not exhaustive: missing variant 'err'",
"line": 9
}
(O código de erro é ilustrativo; o princípio é o contrato.)
Adicione uma variante nova ao choice — digamos Result.timeout: Void — e todo match contra Result na base de código vira um erro de compilação até você tratar o caso novo. Isso é funcionalidade, não chatice: o compilador está te dizendo exatamente onde o caso novo precisa de atenção.
Quando você não precisa do payload
Se o payload da variante é Void ou você simplesmente não se importa com ele neste braço, pode ignorar a ligação — mas ainda precisa escrever o braço para satisfazer a exaustividade:
match token {
.word => w { /* usa w */ }
.number => n { /* usa n */ }
.eof => _ { /* nada para vincular */ }
}
A grafia exata para "ignore o payload" pode evoluir no Zero pré-1.0 (você pode ver _ ou simplesmente omitir a ligação). O ponto conceitual — toda variante recebe um braço, tendo payload ou não — é a parte estável.
Padrões comuns
Um tipo de erro no estilo Result
Esse é exatamente o exemplo que o repositório oficial usa:
choice Result {
ok: i32,
err: String,
}
Funções que podem ter sucesso-com-valor ou falhar-com-mensagem retornam um Result. Quem chama faz pattern matching para extrair o valor ou a mensagem. O sistema raises/check do Zero cuida da propagação de operações falíveis; Result é útil quando você quer segurar um valor de sucesso ou falha como dado.
Um token de parser
choice Token {
word: String,
number: i32,
eof: Void,
}
Um tokenizador produz um fluxo de Tokens. Cada consumidor casa na variante para decidir o que fazer — imprimir a palavra, somar o número, sair em eof.
Uma máquina de estados
choice State {
waiting: Void,
processing: i32,
done: String,
}
processing carrega o ID da tarefa atual; done carrega o resultado final. Cada transição é um novo valor State — sem campos mutáveis espalhados por um shape.
Choice + generics
choice pode ser genérico assim como shape:
choice Maybe<T> {
some: T,
none: Void,
}
Maybe<i32> é "um inteiro opcional". Maybe<String> é "uma string opcional". Esse mesmo padrão aparece na biblioteca padrão do Zero e encaixa muito melhor do que um valor sentinela null — não tem como esquecer o caso .none depois que você faz match sobre o tipo.
Quando usar choice vs. shape vs. enum
Recapitulando rapidamente o que veio de shapes e enums:
- Shape — registro com múltiplos campos, todos juntos.
- Enum — um de N rótulos, sem dado extra.
- Choice — uma de N variantes, cada uma carregando um payload.
A maioria dos modelos de dados num programa real é alguma combinação desses três. A clareza de partir de "isto é e, ou, ou ou com dado?" é um dos benefícios subestimados de trabalhar numa linguagem pequena.
A seguir: a capacidade World
choice e match cobrem o lado dos dados do Zero. O próximo capítulo é sobre efeitos — como programas Zero interagem com o mundo externo. Começa com a capacidade World, o objeto que controla cada pedaço de I/O.
Perguntas frequentes
O que é um choice em Zero?
Um choice é o tipo união marcada do Zero — um valor que é uma de várias variantes nomeadas, cada uma carregando o próprio tipo de payload. Exemplo: choice Result { ok: i32, err: String }. Um valor Result é ou um ok carregando um i32 ou um err carregando uma String. Você constrói um com Result.ok(42) ou Result.err("bad").
Como o match funciona em Zero?
match valor { .varianteA => ligação { ...corpo } .varianteB => ligação { ...corpo } } ramifica sobre qual variante valor carrega. Cada braço casa uma variante, nomeia a ligação do payload e executa o corpo. O compilador verifica que você cobriu todas as variantes — exaustividade é o benefício principal sobre if/else if.
Como você constrói um valor de choice?
Construa nomeando o tipo, a variante e passando o payload: let r: Result = Result.ok(42) ou let r = Result.err("validation failed"). O tipo do payload precisa bater com o payload declarado da variante — passar o tipo errado é erro de compilação.
Qual a diferença entre choice e enum?
Variantes de enum são só rótulos sem payload. Cada variante de choice carrega um valor de um tipo declarado. Se precisa anexar dado a um dos casos (uma mensagem de erro, um resultado de sucesso, um token parseado), use choice. Se os casos são rótulos puros, use enum.
Por que match é preferido sobre if-else para choices?
match é exaustivo por construção — o compilador verifica que toda variante é tratada, então adicionar uma variante nova depois te força a atualizar todo lugar que ramifica sobre o tipo. Uma cadeia if/else if cai em silêncio para o default, escondendo o caso faltante até ele aparecer como bug em produção.