Expresiones condicionales en Scala
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
¿Qué acabamos de hacer?
En este ejemplo, if
retorna directamente el mayor valor entre a
y b
.
No se necesita una variable temporal ni múltiples líneas: la expresión produce el resultado esperado en una sola instrucción.
⚠️ 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: ()
¿Qué acabamos de hacer?
- En ambos casos, el compilador detecta que estás usando
if
como expresión, pero falta la ramaelse
. - Como se espera un valor de tipo
Unit
, Scala convierte implícitamente la expresiónif
en un bloque{ 1; () }
, descartando el valor1
y retornando()
. - El compilador muestra una advertencia:
"Discarded non-Unit value of type Int. Add: Unit
to discard silently." - Aunque el código compila y se ejecuta, el resultado final es siempre
()
, lo que probablemente no sea lo que se pretendía.
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"
¿Qué acabamos de hacer?
Este ejemplo muestra cómo match
puede combinar múltiples técnicas de emparejamiento:
200 | 201 | 204
: uso de patrones alternativos con|
para valores equivalentes.status: Int
: extracción con tipo, que permite manejar enteros no cubiertos antes.status: String
: lo mismo pero para cadenas._
: comodín que actúa como caso por defecto.
Esta estructura es mucho más flexible que switch
o when
, ya que permite trabajar con estructuras complejas, tipos, guardas y más.
🧪 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"
¿Qué acabamos de hacer?
Aquí usamos pattern guards para clasificar códigos HTTP numéricos por rango:
- Cada
case
empareja primero un entero (s: Int
), y luego aplica una condición adicional (if ...
) sobre ese valor. - Esta técnica evita tener que enumerar todos los valores individualmente.
- También puede usarse con cualquier tipo de patrón, no solo enteros.
🧮 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 ...
¿Qué acabamos de hacer?
En este ejemplo, networkStatus
no tiene una rama que maneje objetos arbitrarios.
Al pasarle un Object
, ninguna de las cláusulas coincide, lo que produce un MatchError
.
Para evitarlo, se recomienda:
- Usar un caso
case _ =>
como rama por defecto. - Trabajar con tipos sellados (
sealed
) o enumeraciones, donde el compilador sí puede forzar exhaustividad.
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ística | Kotlin | Scala |
---|---|---|
Naturaleza de if | Declaración o expresión | Siempre expresión |
Rama else en expresiones if | Obligatoria | Opcional (pero recomendada) |
Valor de if sin else | Error de compilación | Retorna Unit (con advertencia) |
Bloques con llaves | Obligatorio para múltiples líneas | Indentación obligatoria (Scala 3) |
Estructura múltiple | when | match |
Retorno de valor en estructuras | if y when pueden retornar valor | if y match retornan valor |
Fall-through en estructuras | No permitido | No permitido |
Patrón comodín | Rama else obligatoria si no exhaustivo | _ opcional (pero recomendado) |
Chequeo de exhaustividad | Automático para when con enums o sealed types | Automático solo con tipos sellados o enums |
Coincidencia estructural avanzada | No soportada (solo básica) | Soportada (destructuración, guardas, coincidencias avanzadas) |
Guardas en patrones | Experimental (-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 expresionesif
, 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 omitirelse
cuando se espera un valor puede llevar a resultados inesperados.- Scala no tiene
when
, peromatch
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.