Enums y pattern matching
¿Qué es un enum?
A veces un valor solo puede ser una de unas pocas opciones específicas. Un semáforo es Rojo, Amarillo o Verde — nunca otra cosa. Un palo de naipes es Corazones, Diamantes, Tréboles o Picas — exactamente cuatro posibilidades.
Un enum (abreviatura de "enumeración") te permite definir un tipo que es exactamente una de un conjunto fijo de variantes.
Definir un enum simple
enum Color { Rojo, Verde, Azul } fn main() { let c: Color = Color.Rojo print(c) // Rojo }
Color es un nuevo tipo con exactamente tres valores posibles. Te refieres a cada variante con un punto: Color.Rojo, Color.Verde, Color.Azul.
Pattern matching con match
El verdadero poder de los enums viene de match — una forma de manejar cada variante de manera diferente:
enum Direccion { Norte, Sur, Este, Oeste } fn describir(d: Direccion) -> String { return match d { Direccion.Norte => "Yendo arriba", Direccion.Sur => "Yendo abajo", Direccion.Este => "Yendo a la derecha", Direccion.Oeste => "Yendo a la izquierda" } } fn main() { print(describir(Direccion.Norte)) // Yendo arriba print(describir(Direccion.Oeste)) // Yendo a la izquierda }
match verifica qué variante es d, y devuelve el valor correspondiente. Es como una serie de if, pero más limpio y el compilador asegura que manejes cada caso.
El patrón comodín
Si no te importan algunas variantes, usa _ como comodín:
enum Color { Rojo, Verde, Azul, Amarillo, Morado } fn es_primario(c: Color) -> bool { return match c { Color.Rojo => true, Color.Verde => true, Color.Azul => true, _ => false } } fn main() { print(es_primario(Color.Rojo)) // true print(es_primario(Color.Amarillo)) // false }
El _ coincide con cualquier variante no listada explícitamente.
Enums con datos
Los enums simples son útiles, pero se vuelven verdaderamente poderosos cuando las variantes llevan datos:
enum Forma { Circulo(int), Rectangulo(int, int), Triangulo(int, int, int) }
Cada variante puede contener diferentes tipos y cantidades de datos. Un Circulo tiene un radio, un Rectangulo tiene ancho y alto, un Triangulo tiene tres lados.
fn area(s: Forma) -> int { return match s { Forma.Circulo(r) => 3 * r * r, Forma.Rectangulo(a, h) => a * h, Forma.Triangulo(a, b, c) => { // Aproximación de fórmula de Herón con aritmética entera let s: int = (a + b + c) / 2 return s * (s - a) / 2 } } } fn describir(s: Forma) -> String { return match s { Forma.Circulo(r) => "Círculo con radio " + int_to_string(r), Forma.Rectangulo(a, h) => "Rectángulo " + int_to_string(a) + "x" + int_to_string(h), Forma.Triangulo(a, b, c) => "Triángulo con lados " + int_to_string(a) + ", " + int_to_string(b) + ", " + int_to_string(c) } } fn main() { let formas: Array = [ Forma.Circulo(10), Forma.Rectangulo(5, 8), Forma.Triangulo(3, 4, 5) ] var i: int = 0 while i < formas.length() { let s: Forma = formas[i] print(describir(s) + " -> area = " + int_to_string(area(s))) i += 1 } }
Salida:
Círculo con radio 10 -> area = 300 Rectángulo 5x8 -> area = 40 Triángulo con lados 3, 4, 5 -> area = 3
Enums para manejo de errores
Uno de los usos más comunes de enums es representar éxito o fallo:
enum Resultado { Ok(int), Error(String) } fn dividir(a: int, b: int) -> Resultado { if b == 0 { return Resultado.Error("División por cero") } return Resultado.Ok(a / b) } fn main() { let r1: Resultado = dividir(10, 3) let r2: Resultado = dividir(10, 0) let msg1: String = match r1 { Resultado.Ok(valor) => "Resultado: " + int_to_string(valor), Resultado.Error(msg) => "Error: " + msg } let msg2: String = match r2 { Resultado.Ok(valor) => "Resultado: " + int_to_string(valor), Resultado.Error(msg) => "Error: " + msg } print(msg1) // Resultado: 3 print(msg2) // Error: División por cero }
En lugar de crashear con entrada inválida, la función devuelve un enum que el llamador debe manejar. Esto hace los errores explícitos e imposibles de ignorar.
Enums para valores opcionales
Otro patrón común — representar "algo o nada":
enum Opcion { Algo(int), Nada } fn buscar_indice(arr: Array, objetivo: int) -> Opcion { var i: int = 0 while i < arr.length() { if arr[i] == objetivo { return Opcion.Algo(i) } i += 1 } return Opcion.Nada } fn main() { let nums: Array = [10, 20, 30, 40, 50] let encontrado: Opcion = buscar_indice(nums, 30) let no_encontrado: Opcion = buscar_indice(nums, 99) let msg1: String = match encontrado { Opcion.Algo(idx) => "Encontrado en índice " + int_to_string(idx), Opcion.Nada => "No encontrado" } let msg2: String = match no_encontrado { Opcion.Algo(idx) => "Encontrado en índice " + int_to_string(idx), Opcion.Nada => "No encontrado" } print(msg1) // Encontrado en índice 2 print(msg2) // No encontrado }
Usar enums como máquinas de estado
Los enums representan estados naturalmente:
enum EstadoPedido { Pendiente, Procesando, Enviado(String), Entregado, Cancelado(String) } fn describir_pedido(estado: EstadoPedido) -> String { return match estado { EstadoPedido.Pendiente => "Pedido pendiente", EstadoPedido.Procesando => "Pedido en proceso", EstadoPedido.Enviado(tracking) => "¡Enviado! Seguimiento: " + tracking, EstadoPedido.Entregado => "Pedido entregado", EstadoPedido.Cancelado(razon) => "Cancelado: " + razon } } fn main() { let s1: EstadoPedido = EstadoPedido.Pendiente let s2: EstadoPedido = EstadoPedido.Enviado("NYX-12345") let s3: EstadoPedido = EstadoPedido.Cancelado("Sin stock") print(describir_pedido(s1)) // Pedido pendiente print(describir_pedido(s2)) // ¡Enviado! Seguimiento: NYX-12345 print(describir_pedido(s3)) // Cancelado: Sin stock }
Ejemplo práctico: una calculadora
enum Operacion { Sumar(int, int), Restar(int, int), Multiplicar(int, int), Dividir(int, int) } fn calcular(op: Operacion) -> int { return match op { Operacion.Sumar(a, b) => a + b, Operacion.Restar(a, b) => a - b, Operacion.Multiplicar(a, b) => a * b, Operacion.Dividir(a, b) => a / b } } fn mostrar(op: Operacion) -> String { return match op { Operacion.Sumar(a, b) => int_to_string(a) + " + " + int_to_string(b), Operacion.Restar(a, b) => int_to_string(a) + " - " + int_to_string(b), Operacion.Multiplicar(a, b) => int_to_string(a) + " * " + int_to_string(b), Operacion.Dividir(a, b) => int_to_string(a) + " / " + int_to_string(b) } } fn main() { let ops: Array = [ Operacion.Sumar(10, 5), Operacion.Restar(10, 5), Operacion.Multiplicar(10, 5), Operacion.Dividir(10, 5) ] var i: int = 0 while i < ops.length() { let op: Operacion = ops[i] print(mostrar(op) + " = " + int_to_string(calcular(op))) i += 1 } }
Salida:
10 + 5 = 15 10 - 5 = 5 10 * 5 = 50 10 / 5 = 2
Ejercicios
- Define un enum
DiaSemanacon los siete días. Escribe una funciónes_fin_de_semana(d: DiaSemana) -> boolque devuelvatruepara Sábado y Domingo.
- Define un enum
Monedacon variantesCentavo,Cinco,Diez,Veinticinco. Escribevalor(m: Moneda) -> intque devuelva el valor de la moneda en centavos.
- Define
enum Temperatura { Celsius(int), Fahrenheit(int) }. Escribea_celsius(t: Temperatura) -> intque convierta Fahrenheit a Celsius (usa aritmética entera:(f - 32) * 5 / 9).
- Crea un enum
Resultadoy escribe una funcióndivision_seguraque devuelvaResultado.Errorpara división por cero. Encadena dos divisiones: divide 100 entre 5, luego divide el resultado entre 0.
- Construye un evaluador de expresiones simple:
enum Expr { Num(int), Suma(int, int), Mul(int, int), Neg(int) }. Escribeevaluar(e: Expr) -> intque calcule el resultado.
Resumen
enum Nombre { Variante1, Variante2, ... }define un tipo con variantes fijas.- Accede a variantes con notación de punto:
Color.Rojo. - Las variantes pueden llevar datos:
Forma.Circulo(10). matchinspecciona qué variante es un valor y extrae sus datos._es un comodín que coincide con cualquier variante no manejada.- Patrones comunes:
Result(Ok/Error) para manejo de errores,Option(Some/None) para valores opcionales. - Enums + match = manejo limpio y seguro de múltiples casos.
Siguiente capítulo: Traits y bloques impl →