Índice

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

  1. Define un enum DiaSemana con los siete días. Escribe una función es_fin_de_semana(d: DiaSemana) -> bool que devuelva true para Sábado y Domingo.
  1. Define un enum Moneda con variantes Centavo, Cinco, Diez, Veinticinco. Escribe valor(m: Moneda) -> int que devuelva el valor de la moneda en centavos.
  1. Define enum Temperatura { Celsius(int), Fahrenheit(int) }. Escribe a_celsius(t: Temperatura) -> int que convierta Fahrenheit a Celsius (usa aritmética entera: (f - 32) * 5 / 9).
  1. Crea un enum Resultado y escribe una función division_segura que devuelva Resultado.Error para división por cero. Encadena dos divisiones: divide 100 entre 5, luego divide el resultado entre 0.
  1. Construye un evaluador de expresiones simple: enum Expr { Num(int), Suma(int, int), Mul(int, int), Neg(int) }. Escribe evaluar(e: Expr) -> int que calcule el resultado.

Resumen

Siguiente capítulo: Traits y bloques impl →

← Anterior: Closures y funciones de primera clase Siguiente: Traits e impl blocks →