Menu

Choice y match en Zero: uniones etiquetadas y pattern matching

Cómo choice declara una unión etiquetada en Zero y cómo match ramifica de forma exhaustiva sobre sus variantes — la versión de Zero de los tipos suma y el pattern matching.

Esta página incluye editores ejecutables: edita, ejecuta y ve el resultado al instante.

Uniones etiquetadas, al estilo Zero

choice declara un tipo cuyo valor es uno de varios variantes nombradas, llevando cada una su propio payload:

choice Result {
    ok:  i32,
    err: String,
}

Un valor Result es o bien un ok que lleva un i32 o bien un err que lleva un String. Nunca ambos, nunca ninguno. El sistema de tipos hace esa garantía, y match la hace cómoda de aprovechar.

Es la misma idea que en otros lenguajes se llama "unión etiquetada", "tipo suma", "unión discriminada" o "tipo algebraico". Zero la escribe choice y mantiene la gramática pequeña.

Declarar un choice

choice Name {
    variantA: PayloadTypeA,
    variantB: PayloadTypeB,
}

Cada línea lista una variante. El nombre es tuyo; el tipo tras los dos puntos es el payload que lleva esa variante. Una variante que no necesita payload usa Void:

choice Token {
    word:   String,
    number: i32,
    eof:    Void,
}

Token.eof es una variante sin payload útil (su tipo de payload es Void) — útil para casos tipo terminador.

Construir un valor de choice

Construye un valor nombrando el tipo, luego la variante, y luego pasando el payload:

let success = Result.ok(42)
let failure = Result.err("validación fallida")

El tipo del payload tiene que coincidir con el payload declarado para la variante. Result.ok("hola") sería un error de compilación porque ok espera un i32.

La inferencia de tipos también funciona aquí. Si el lado derecho determina por completo el tipo, puedes escribir let success = Result.ok(42) y el tipo de la ligadura es Result. Anotar el tipo explícitamente está bien cuando quieres documentarlo en el sitio de la ligadura:

let success: Result = Result.ok(42)

Hacer match sobre un choice

match es la forma de leer un valor choice. La forma:

match value {
    .variantA => binding { /* cuerpo cuando value es variantA, con el payload en `binding` */ }
    .variantB => binding { /* cuerpo cuando value es variantB */ }
}

Un ejemplo trabajado del repositorio oficial de Zero — pulsa Ejecutar para ver disparar la rama .ok:

Lee el match literal: "dependiendo de qué variante tenga result, ejecuta la rama coincidente y vincula el payload al nombre elegido." En la rama .ok, value es el payload i32. En la rama .err, message es el payload String. Cada rama es un ámbito separado; la ligadura solo es visible dentro de su propio cuerpo.

Exhaustividad

Esta es la gran ventaja de match sobre las cadenas if/else if: el compilador verifica que cada variante tiene una rama. Si te olvidas del caso .err, no obtienes una caída en tiempo de ejecución a una rama por defecto — obtienes un error de compilación:

{
    "code": "MAT001",
    "message": "match is not exhaustive: missing variant 'err'",
    "line": 9
}

(El código de error es ilustrativo; el principio es el contrato.)

Añade una variante nueva al choice — digamos Result.timeout: Void — y cada match contra Result en la base de código se convierte en un error de compilación hasta que manejes el nuevo caso. Eso es una funcionalidad, no una carga: el compilador te está diciendo exactamente dónde el nuevo caso necesita atención.

Cuando no necesitas el payload

Si el payload de una variante es Void o simplemente no te importa en esta rama, puedes ignorar la ligadura — pero igualmente tienes que escribir la rama para satisfacer la exhaustividad:

match token {
    .word   => w { /* usa w */ }
    .number => n { /* usa n */ }
    .eof    => _ { /* nada que ligar */ }
}

La forma exacta de "ignorar el payload" puede evolucionar en Zero pre-1.0 (puede que veas _ o simplemente omitir la ligadura). El punto conceptual — cada variante recibe una rama, con o sin payload — es la parte estable.

Patrones habituales

Un tipo de error estilo Result

Este es exactamente el ejemplo que usa el repositorio oficial:

choice Result {
    ok:  i32,
    err: String,
}

Las funciones que pueden tener éxito-con-valor o fallar-con-mensaje devuelven un Result. Quienes llaman hacen pattern matching para extraer o bien el valor o bien el mensaje. El sistema raises/check de Zero gestiona la propagación para operaciones falibles; Result es útil cuando quieres retener un valor de éxito-o-fallo como datos.

Un token de parser

choice Token {
    word:   String,
    number: i32,
    eof:    Void,
}

Un tokenizador produce un flujo de Tokens. Cada consumidor hace match sobre la variante para decidir qué hacer: imprimir la palabra, sumar el número, salir en eof.

Una máquina de estados

choice State {
    waiting:    Void,
    processing: i32,
    done:       String,
}

processing lleva el ID de la tarea actual; done lleva el resultado final. Cada transición es un nuevo valor State — sin campos mutables esparcidos por un shape.

Choice + genéricos

choice puede ser genérico igual que shape:

choice Maybe<T> {
    some: T,
    none: Void,
}

Maybe<i32> es "un entero opcional". Maybe<String> es "una cadena opcional". Este mismo patrón aparece en la biblioteca estándar de Zero y encaja mucho mejor que un valor centinela nulo: no hay forma de olvidar el caso .none una vez que haces match contra el tipo.

Cuándo usar choice vs. shape vs. enum

Recordatorio rápido de shapes y enums:

  • Shape — registro con varios campos, todos presentes a la vez.
  • Enum — uno de N etiquetas, sin datos extra.
  • Choice — una de N variantes, cada una llevando un payload.

La mayoría de los modelos de datos en un programa real son alguna combinación de estos tres. La claridad de empezar desde "¿es esto y, o u o con datos?" es uno de los beneficios infravalorados de trabajar en un lenguaje pequeño.

Lo siguiente: la capacidad World

choice y match cubren el lado de los datos de Zero. El próximo capítulo trata sobre los efectos — cómo interactúan los programas de Zero con el mundo exterior. Empieza con la capacidad World, el objeto que controla cada pieza de E/S.

Preguntas frecuentes

¿Qué es un choice en Zero?

Un choice es el tipo unión etiquetada de Zero — un valor que es uno de varios variantes nombradas, cada variante con su propio tipo de payload. Ejemplo: choice Result { ok: i32, err: String }. Un valor Result es o bien un ok con un i32 o un err con un String. Se construye con Result.ok(42) o Result.err("mal").

¿Cómo funciona match en Zero?

match value { .variantA => binding { ...body } .variantB => binding { ...body } } ramifica según qué variante contiene value. Cada rama hace pattern matching contra una variante, nombra la ligadura del payload y ejecuta su cuerpo. El compilador verifica que has cubierto cada variante — la exhaustividad es el beneficio principal frente a if/else if.

¿Cómo se construye un valor de choice?

Se construye nombrando el tipo y la variante y pasando el payload: let r: Result = Result.ok(42) o let r = Result.err("validación fallida"). El tipo del payload tiene que coincidir con el declarado en la variante: pasar un tipo equivocado es un error de compilación.

¿Cuál es la diferencia entre choice y enum?

Las variantes de enum son solo etiquetas sin payload. Las variantes de choice llevan cada una un valor del tipo declarado. Si necesitas adjuntar datos a alguno de los casos (un mensaje de error, un resultado exitoso, un token parseado), usa choice. Si los casos son puras etiquetas, usa enum.

¿Por qué se prefiere match sobre if-else para choices?

match es exhaustivo por construcción — el compilador comprueba que se maneja cada variante, así que añadir una variante nueva más adelante te obliga a actualizar cada sitio que ramifica sobre el tipo. Una cadena if/else if cae silenciosamente, ocultando el caso faltante hasta que aparece como bug en producción.

Coddy programming languages illustration

Aprende a programar con Coddy

COMENZAR