Enums y pattern matching en Scala 3: ADTs, sealed traits y exhaustividad en la práctica

Scala siempre ha tenido un pattern matching potente, pero Scala 3 lo lleva un paso más allá con los enums propios del lenguaje y con mejoras en la verificación de exhaustividad. Si modelas dominios de negocio o trabajas con tipos algebraicos, estos son probablemente los cambios que más impactan en el código del día a día.

Enums en Scala 3

Scala 2 no tenía enums propios: se usaba una sealed trait con case objects, o la workaround de Enumeration (que tenía varios problemas de tipado). Scala 3 introduce enums como construcción de primera clase:

// Enum simple
enum Color:
  case Rojo, Verde, Azul

// Enum con parámetros
enum Forma(val lados: Int):
  case Circulo    extends Forma(0)
  case Triangulo  extends Forma(3)
  case Cuadrado   extends Forma(4)

// Enum con métodos
enum Direccion:
  case Norte, Sur, Este, Oeste

  def opuesta: Direccion = this match
    case Norte => Sur
    case Sur   => Norte
    case Este  => Oeste
    case Oeste => Este

Los enums en Scala 3 son mucho más que constantes: pueden tener parámetros, métodos y extender traits. Internamente, cada case de un enum es una instancia de la clase del enum, así que son compatibles con el sistema de tipos completo.

ADTs con sealed trait y case class

Los Algebraic Data Types (ADTs) son la forma de modelar datos que pueden tener varias formas. En Scala esto se hace con sealed trait y case class, que siguen siendo válidos y muy usados en Scala 3:

sealed trait Resultado[+A]
case class Exito[A](valor: A)     extends Resultado[A]
case class Fallo(mensaje: String) extends Resultado[Nothing]
case object Pendiente              extends Resultado[Nothing]

def procesar(r: Resultado[Int]): String = r match
  case Exito(n)      => s"Valor obtenido: $n"
  case Fallo(msg)    => s"Error: $msg"
  case Pendiente     => "Todavía procesando..."

El compilador sabe que Resultado solo puede ser Exito, Fallo o Pendiente (porque el trait es sealed), así que si te olvidas de un caso en el match, te avisa en compilación. Esta verificación de exhaustividad es una de las características más valiosas de Scala para modelado de dominio.

Pattern matching avanzado

El pattern matching de Scala 3 va mucho más allá de un switch mejorado. Puedes hacer match sobre tipos, tuplas, estructuras anidadas y añadir guards:

sealed trait Nodo
case class Hoja(valor: Int)                     extends Nodo
case class Rama(izq: Nodo, valor: Int, der: Nodo) extends Nodo

def sumar(nodo: Nodo): Int = nodo match
  case Hoja(v)        => v
  case Rama(i, v, d)  => v + sumar(i) + sumar(d)

// Pattern con guard
def clasificar(n: Int): String = n match
  case 0              => "cero"
  case n if n < 0    => s"negativo: $n"
  case n if n > 100  => s"grande: $n"
  case n              => s"normal: $n"

// @-binding: captura el valor mientras aplicas un patrón
def describir(lista: List[Int]): String = lista match
  case Nil           => "lista vacía"
  case head :: Nil   => s"un elemento: $head"
  case head :: resto => s"empieza con $head, tiene ${resto.length} más"

// Match sobre tipos
def describeTipo(x: Any): String = x match
  case s: String => s"texto: $s"
  case i: Int    => s"entero: $i"
  case _         => "otra cosa"

Match expressions: todo es una expresión

En Scala, match es una expresión que devuelve un valor, no una sentencia. Esto permite usarlo directamente en asignaciones, en parámetros de funciones o en for comprehensions:

val descripcion: String =
  List(1, 2, 3) match
    case Nil  => "vacía"
    case list => s"${list.length} elementos"

// En una función directamente
def etiqueta(color: Color): String = color match
  case Color.Rojo  => "#FF0000"
  case Color.Verde => "#00FF00"
  case Color.Azul  => "#0000FF"

Enums vs sealed trait: cuándo usar cada uno

La guía práctica es esta: usa enum cuando los casos son simples (constantes con o sin parámetros fijos) y todos tienen la misma estructura. Usa sealed trait con case class cuando los casos tienen estructuras muy distintas o cuando necesitas covarianza/contravarianza en el tipo.

En la práctica, modelar errores de dominio con enums es muy limpio:

enum ErrorPago:
  case SaldoInsuficiente(disponible: Double, requerido: Double)
  case TarjetaExpirada(fechaExpiracion: String)
  case LimiteExcedido(limite: Double)
  case TransaccionRechazada(motivo: String)

def procesarPago(importe: Double): Either[ErrorPago, String] =
  if importe > 1000.0 then
    Left(ErrorPago.LimiteExcedido(1000.0))
  else
    Right(s"Pago de $importe procesado")

Este patrón de errores como valores (usando Either o tipos propios) es central en Scala funcional, y los enums de Scala 3 lo hacen más ergonómico que nunca. La exhaustividad del compilador garantiza que nunca te olvides de manejar un caso de error, que es exactamente el tipo de bug que más cuesta encontrar en producción.

Imagen: Pexels / Sabrina Gelbart

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP