Menu

Choice e Match no Zero: uniões marcadas e pattern matching

Como choice declara uma união marcada em Zero e como match ramifica de forma exaustiva sobre as variantes — a versão Zero de tipos soma e pattern matching.

Esta página tem editores executáveis — edite, execute e veja a saída na hora.

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.

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR