Skip to main content

Expresiones condicionales en Scala

r8vnhill/scala-dibs

Scala es un lenguaje expresivo y funcional donde incluso las construcciones de control más tradicionales, como if y match, se comportan como expresiones: devuelven un valor y pueden componer lógica declarativa sin efectos secundarios.

En esta lección exploraremos cómo Scala trata las decisiones condicionales de forma más rica que otros lenguajes como Java o Kotlin. Aprenderás a escribir if como una verdadera expresión, a evitar errores comunes como omitir la rama else, y a usar match para construir flujos de decisión potentes y seguros, incluyendo técnicas como guardas y coincidencia estructural.

Estas capacidades son fundamentales para diseñar bibliotecas expresivas, seguras y fáciles de mantener —y te prepararán para modelar lógica más avanzada con tipos algebraicos.

🔀 if siempre es una expresión

En Scala, if es siempre una expresión: produce un valor y no se limita a controlar el flujo del programa.
Esto permite escribir funciones concisas y expresivas, sin necesidad de bloques adicionales para calcular y luego retornar un valor.

def maxOf(a: Int, b: Int): Int = if a > b then a else b

⚠️ Omitir else puede generar resultados confusos

Aunque if siempre es una expresión en Scala, omitir la rama else cuando se espera un valor no invalida el programa, pero sí produce un comportamiento inesperado y un warning del compilador.

val x: Unit = if 1 > 2 then 1
val y: Unit = if 2 > 1 then 1
println(x) // Prints: ()
println(y) // Prints: ()

Mejores prácticas

Si estás usando if como expresión, nunca omitas la rama else cuando esperas que produzca un valor.
Esto previene advertencias, evita que valores útiles sean descartados silenciosamente, y hace que la intención del código sea explícita.

🎛️ match como alternativa a when

Scala no tiene una construcción when como Kotlin, pero ofrece algo más potente: el patrón de emparejamiento (match).
match permite comparar un valor contra múltiples patrones y ejecutar diferentes ramas según corresponda. A diferencia de switch en otros lenguajes, es más expresivo y seguro si se usa correctamente.

status match
case 200 | 201 | 204 => "Success"
case 400 => "Bad Request"
case 404 => "Not Found"
case 500 => "Internal Server Error"
case "timeout" => "The request timed out"
case status: Int => s"Unhandled status code: $status"
case status: String => s"Unhandled string: $status"
case _ => "Unknown type"

🧪 Pattern guards

Scala permite refinar aún más los patrones usando pattern guards, una condición adicional escrita con if después del patrón.
Esto permite, por ejemplo, distinguir ciertos enteros o validar condiciones más complejas:

status match
case s: Int if s >= 200 && s < 300 => "Success"
case s: Int if s >= 400 && s < 500 => "Client error"
case s: Int if s >= 500 => "Server error"
case _ => "Unknown"

🧮 match no siempre es exhaustivo

A diferencia de Kotlin, Scala no fuerza la exhaustividad en todas las situaciones.
Si no se cubren todos los casos posibles y no se incluye un comodín (_), se corre el riesgo de un error en tiempo de ejecución llamado MatchError.

networkStatus(Object())
Exception in thread "main" scala.MatchError: java.lang.Object@... (of class java.lang.Object)
at ...

Mejores prácticas

Siempre que uses match, asegúrate de cubrir todos los casos posibles, ya sea mediante patrones específicos o con un comodín _.
Si trabajas con tus propios tipos, usa sealed trait o enum para que el compilador pueda ayudarte a detectar casos no cubiertos.

🧩 Pattern matching

Los ejemplos que vimos hasta ahora son casos simples de pattern matching: comparar valores literales o tipos básicos.
Sin embargo, una de las fortalezas más destacadas de Scala es su capacidad para realizar coincidencia estructural y destructuración de objetos y colecciones.

Más adelante, cuando veamos tipos algebraicos como case class, enum o jerarquías con sealed trait, aprenderemos cómo match puede descomponer estructuras complejas, extraer valores internos, e incluso aplicar condiciones personalizadas.

Esto permite escribir código conciso, seguro y expresivo, especialmente útil al diseñar bibliotecas y trabajar con estructuras de datos ricas.

📊 Resumen comparativo

CaracterísticaKotlinScala
Naturaleza de ifDeclaración o expresiónSiempre expresión
Rama else en expresiones ifObligatoriaOpcional (pero recomendada)
Valor de if sin elseError de compilaciónRetorna Unit (con advertencia)
Bloques con llavesObligatorio para múltiples líneasIndentación obligatoria (Scala 3)
Estructura múltiplewhenmatch
Retorno de valor en estructurasif y when pueden retornar valorif y match retornan valor
Fall-through en estructurasNo permitidoNo permitido
Patrón comodínRama else obligatoria si no exhaustivo_ opcional (pero recomendado)
Chequeo de exhaustividadAutomático para when con enums o sealed typesAutomático solo con tipos sellados o enums
Coincidencia estructural avanzadaNo soportada (solo básica)Soportada (destructuración, guardas, coincidencias avanzadas)
Guardas en patronesExperimental (-Xwhen-guards)Soportado plenamente (if)

Beneficios de Scala

  • if siempre es una expresión, lo que simplifica la lógica al permitir un estilo más funcional y menos imperativo.
  • match ofrece patrones avanzados como la coincidencia estructural, destructuración y guardas, lo que permite un control de flujo más potente y expresivo.
  • Unión de tipos (Int | String) que mejora la flexibilidad al evaluar condiciones con valores de tipos distintos.
  • El pattern matching estructural permite trabajar fácilmente con estructuras de datos complejas como listas, tuplas y case classes.
  • Uso de guardas en patrones (case ... if) para evaluar condiciones adicionales, haciendo el código más conciso y legible.

Limitaciones de Scala

  • La rama else es opcional en expresiones if, lo que puede causar errores inadvertidos al retornar valores inesperados (Unit).
  • No fuerza exhaustividad en todos los casos del patrón match, lo que puede llevar a errores en tiempo de ejecución (MatchError) si no se usa correctamente.
  • El patrón comodín (_) es opcional, por lo que queda en manos del desarrollador asegurar que todos los casos estén cubiertos, aumentando el riesgo de errores inadvertidos.
  • Aunque flexible, el uso avanzado del pattern matching puede generar código difícil de leer si se abusa de patrones complejos o guardas demasiado intrincadas.

🎯 Conclusiones

Scala promueve un estilo funcional y expresivo donde incluso las construcciones de control tradicionales como if y match son expresiones que retornan valores. Esto permite componer lógica de forma más declarativa y concisa, pero también implica nuevas responsabilidades: el compilador permite más, pero espera que quien escribe el código sea más explícito y cuidadoso.

El sistema de patrones de Scala —junto con sus guardas y coincidencia estructural— abre la puerta a un estilo poderoso y seguro de programación, especialmente útil al diseñar bibliotecas con estructuras de datos complejas. No obstante, este poder requiere atención: omitir un caso en match o no usar un else puede derivar en errores sutiles.

🔑 Puntos clave

  • if siempre es una expresión y puede devolver un valor, pero omitir else cuando se espera un valor puede llevar a resultados inesperados.
  • Scala no tiene when, pero match lo supera ampliamente en flexibilidad y expresividad.
  • Se pueden usar tipos alternativos, destructuración y pattern guards para expresar condiciones de forma elegante.
  • El compilador solo fuerza exhaustividad con tipos sellados o enumeraciones, por lo que es buena práctica incluir un comodín (_) para evitar errores en tiempo de ejecución.

🧰 ¿Qué nos llevamos?

Aprendimos que en Scala el control de flujo no es una excepción al diseño expresivo y funcional del lenguaje: tanto if como match pueden usarse para devolver valores, construir lógica compleja sin efectos secundarios y modelar decisiones con precisión.

Además, vimos cómo match no se limita a simples comparaciones, sino que permite construir patrones ricos, validar condiciones con guardas y desestructurar estructuras complejas. Estas capacidades son fundamentales para escribir código expresivo, seguro y reutilizable en bibliotecas funcionales.

Esta base será esencial para lo que viene: modelar datos con tipos algebraicos y aprovechar aún más el poder de los patrones.