case ... of: el pattern matching que el compilador vigila
Si type es la forma de construir un valor de una unión etiquetada, case ... of es la única forma de desarmarlo: pregunta por la variante y, en la misma operación, extrae los datos que esa variante transporta. Esta lección desarrolla el pattern matching de Elm como expresión y no como sentencia, la anatomía de sus ramas, la significación de las variables de patrón que ligan la carga del constructor, y sobre todo la propiedad que lo convierte en una herramienta de ingeniería y no solo de sintaxis: la exhaustividad. El compilador de Elm conoce el conjunto cerrado de constructores, cuenta las ramas y se niega a compilar si falta una, con lo que añadir una variante a un tipo deja de ser un cambio arriesgado y se convierte en una lista de tareas que el compilador te dicta. Se examina el mensaje de error real, el papel y el peligro del comodín, la diferencia entre no compilar y fallar en producción, y por qué la exhaustividad estructural es la mecánica concreta detrás de la fama de que en Elm si compila funciona.
Un tipo suma se construye eligiendo un constructor; ahora hace falta el movimiento inverso, el de averiguar cuál se eligió y recuperar lo que traía dentro. Ese movimiento es case ... of, y en Elm no es una alternativa cómoda al condicional sino el único camino: no existe forma de leer los datos de una variante sin pasar por él, ni de preguntar por el caso sin comprometerse a decir qué se hace en los demás. Esa exclusividad es deliberada y es la que hace que todo el edificio se sostenga. Porque case no se limita a ramificar; el compilador conoce el conjunto completo de constructores del tipo —lo conoce porque type lo declaró cerrado—, cuenta las ramas que has escrito y compara. Si falta una, el programa no compila. No advierte, no sugiere, no lo deja pasar con un aviso amarillo: se niega. Esta lección trata de esa negativa, que a primera vista parece una molestia y a los quince días parece lo mejor del lenguaje.
- Escribir
case ... ofcomo expresión que devuelve un valor y cuyas ramas comparten un mismo tipo. - Ligar en el propio patrón los datos que transporta cada constructor, sin conversiones ni comprobaciones.
- Explicar cómo el compilador verifica la exhaustividad y qué garantiza exactamente esa verificación.
- Usar el comodín con criterio, sabiendo qué comprobación futura desactiva al escribirlo.
Anatomía de una expresión case
La forma es sobria: la palabra case, la expresión que se examina, la palabra of, y debajo una rama por caso, cada una con un patrón, una flecha y el valor que produce. Lo primero que hay que interiorizar es que se trata de una expresión, no de una sentencia. Todo case devuelve un valor, todas sus ramas deben devolver valores del mismo tipo, y no existe ningún camino que no devuelva nada.
type EstadoCarga
= Inactivo
| Cargando
| Exito (List Usuario)
| Error Http.Error
mensaje : EstadoCarga -> String
mensaje estado =
case estado of
Inactivo ->
"Todavia no se ha pedido nada"
Cargando ->
"Cargando..."
Exito usuarios ->
"Han llegado " ++ String.fromInt (List.length usuarios) ++ " usuarios"
Error _ ->
"Ha fallado la peticion"
En la rama de Exito, la palabra usuarios no es una comprobación sino una ligadura: el patrón Exito usuarios dice a la vez si el valor fue construido con Exito, entonces llama usuarios a lo que llevaba dentro. Esa doble función —discriminar y extraer en un solo gesto— es lo que distingue el pattern matching de un condicional seguido de un acceso. No hay conversión de tipos, no hay comprobación previa, no hay posibilidad de equivocarse de campo: dentro de esa rama, y solo dentro de ella, existe una List Usuario llamada usuarios, con ese tipo garantizado por construcción.
El guion bajo de la última rama es el comodín: casa con cualquier cosa y no liga nada. Usarlo para un dato que no piensas mirar es correcto y hasta educado, porque comunica al lector que la información existe pero no importa aquí. Usarlo para casos enteros, como veremos, es otra cosa muy distinta.
El orden importa cuando los patrones se solapan, cosa que ocurre en cuanto mezclas literales con variables. Un patrón que sea una variable simple casa con todo, de modo que cualquier rama escrita debajo de él es inalcanzable, y Elm te lo dirá. La lectura práctica es que los patrones deben ordenarse de lo más específico a lo más general, exactamente como se lee una cascada de condiciones. Con constructores puros el orden es irrelevante, porque son mutuamente excluyentes por definición y ninguno puede tapar a otro.
La exhaustividad: el compilador cuenta
Aquí está el corazón de la lección. Supón que el producto crece y el estado de carga necesita un quinto caso, porque ahora una petición se puede cancelar. Añades el constructor y no tocas nada más.
type EstadoCarga
= Inactivo
| Cargando
| Exito (List Usuario)
| Error Http.Error
| Cancelado
El proyecto deja de compilar de inmediato, y no en un sitio sino en todos. Cada case sobre EstadoCarga repartido por la aplicación se convierte en un error con la misma forma: este case no tiene ramas para todas las posibilidades, falta Cancelado. El compilador no ha adivinado nada ni ha corrido ninguna prueba; simplemente sabe que el tipo tiene cinco constructores porque el tipo es cerrado, ha contado cuatro ramas y ha restado.
Conviene apreciar lo que eso significa en la práctica de un equipo. Sin exhaustividad, añadir una variante es una de las operaciones más peligrosas que existen: el código compila, se despliega, y en algún rincón que nadie recordaba hay un condicional que trata el caso nuevo como si fuera otro, o una excepción que salta el martes siguiente. Encontrar todos los sitios afectados es un trabajo de búsqueda de texto y memoria, y la memoria falla. Con exhaustividad, la operación se invierte por completo: el compilador te entrega la lista exacta y completa de lugares donde falta una decisión, y no te deja terminar hasta que la hayas tomado en todos. El cambio ya no es arriesgado, es tedioso, y esa rebaja de categoría es una de las mejores cosas que le pueden pasar a una base de código que dura años.
flowchart TD T[type con cinco constructores] --> C[case con cuatro ramas] C --> Q[El compilador cuenta y compara] Q -->|falta un caso| E[Error de compilacion con el nombre del caso] Q -->|estan todos| O[Compila y la funcion es total] style Q fill:#f9e2af,color:#11111b style E fill:#f38ba8,color:#11111b style O fill:#a6e3a1,color:#11111b
Discriminar y extraer
El patrón pregunta por el constructor y liga su carga en un solo paso, con el tipo ya garantizado.
Exhaustividad
El conjunto de constructores es cerrado, luego contable. El compilador compara ramas con casos y se niega si faltan.
Refactor guiado
Añadir una variante convierte el compilador en la lista de tareas exacta de todo lo que hay que revisar.
El comodín
Casa con todo y por eso apaga la comprobación futura. Cómodo hoy, silencioso mañana.
Hay un matiz que conviene fijar antes de seguir, porque separa esta comprobación de otras que se le parecen. La exhaustividad de Elm no es un aviso configurable ni una regla de linter que un equipo pueda desactivar cuando le estorbe. Está en el compilador, es obligatoria y no admite excepción, y esa rigidez es justamente lo que la hace útil, porque una garantía que se puede apagar deja de ser una garantía y pasa a ser una sugerencia. En lenguajes que ofrecen comprobaciones parecidas como opción —una bandera del compilador, una regla que hay que activar— lo habitual es que el aviso exista y que nadie lo lea, sepultado entre otros doscientos. Aquí no hay nada que leer: o compila o no compila.
-- La misma funcion, ahora con el caso nuevo tratado explicitamente
mensaje : EstadoCarga -> String
mensaje estado =
case estado of
Inactivo ->
"Todavia no se ha pedido nada"
Cargando ->
"Cargando..."
Exito usuarios ->
"Han llegado " ++ String.fromInt (List.length usuarios) ++ " usuarios"
Error _ ->
"Ha fallado la peticion"
Cancelado ->
"La peticion se cancelo"
El comodín, o cómo renunciar voluntariamente a la garantía
Elm permite cerrar un case con una rama comodín que cubra todo lo restante, y esa posibilidad merece un aviso serio, porque es el único modo de perder lo que acabamos de ganar.
-- Comodo hoy: cubre lo que queda sin nombrarlo
esFinal : EstadoCarga -> Bool
esFinal estado =
case estado of
Exito _ ->
True
_ ->
False
Esa función es correcta hoy. El problema es lo que ocurre mañana, cuando alguien añada Cancelado al tipo: esFinal seguirá compilando, sin una sola queja, y devolverá False para un estado que probablemente sea el más final de todos. El comodín no ha introducido un error; ha desactivado el mecanismo que lo habría señalado. Ha convertido un fallo de compilación, que es barato y ocurre en tu máquina, en un fallo de comportamiento, que es caro y ocurre en la de un usuario.
La regla práctica que emplean los equipos con experiencia es enunciable en una línea: usa el comodín para ignorar datos dentro de un patrón, casi nunca para agrupar casos enteros. Cuando la alternativa —enumerar los cuatro casos que devuelven False— resulta pesada, esa pesadez suele estar diciéndote algo del modelo, no del case; a menudo hay ahí un tipo intermedio esperando a ser extraído. Y cuando el comodín sea de verdad lo correcto, escríbelo sabiendo qué has apagado, porque la diferencia entre un buen programador y uno descuidado no es que el primero nunca use el comodín, sino que sabe exactamente qué comprobación futura está renunciando a tener.
La garantía es estructural, y conviene no exagerarla. El compilador verifica que has escrito una rama para cada constructor, no que el valor devuelto en cada rama sea el que el negocio esperaba. Puedes tratar los cinco casos y equivocarte en los cinco, y el programa compilará impecablemente. Lo que la exhaustividad elimina es una clase entera y muy concreta de errores —el caso olvidado, la rama que nadie escribió, el estado que nadie previó al cambiar el tipo— que además es la clase que más sobrevive a las revisiones de código, porque revisar consiste en leer lo que hay y estos errores consisten precisamente en lo que no está.
Sobre Elm circula una frase que suena a exageración publicitaria: si compila, funciona. Como toda consigna, es falsa tomada al pie de la letra y muy informativa entendida bien, y case es el sitio donde se ve de qué está hecha. La afirmación honesta sería otra: en Elm, ciertas clases enteras de errores no llegan a existir, y la comprobación de exhaustividad es una de las tres o cuatro piezas que las eliminan. Merece la pena entender por qué el mecanismo es tan barato y tan potente a la vez, porque la respuesta es casi decepcionante de simple: el conjunto de constructores es finito y cerrado, y sobre un conjunto finito y cerrado se puede contar. Toda la garantía sale de ahí. No hay análisis de flujo, ni demostración de teoremas, ni inferencia sofisticada: hay una enumeración que el tipo declaró y una comparación que el compilador hace. Compáralo con lo que ocurre donde el conjunto no es cerrado. Un switch sobre cadenas de texto en JavaScript no puede comprobar nada, porque el conjunto de cadenas es infinito y el compilador no tiene forma de saber cuáles pretendías. Una jerarquía de clases abierta no puede comprobar nada, porque mañana alguien añadirá una subclase en otro módulo y ninguna comprobación local podría anticiparlo; por eso los lenguajes orientados a objetos que quisieron esta garantía tuvieron que inventar sealed y final, es decir, tuvieron que cerrar el conjunto a mano para poder contar. Un default en un switch de C compila siempre y es el equivalente exacto del comodín, con el mismo efecto sedante. Y el enum de C ni siquiera protege, porque un enum es un entero con nombres bonitos y nada impide que llegue un valor fuera de la lista. La lección de ingeniería que se lleva uno de aquí trasciende Elm por completo, y es esta: la verificabilidad de un programa es proporcional a cuánto se haya cerrado sobre lo que puede ocurrir. Cada vez que dejas abierto el conjunto de posibilidades —texto libre, jerarquías extensibles, campos que admiten cualquier cosa, mapas de claves arbitrarias— compras flexibilidad y vendes comprobación, y la venta es silenciosa porque nadie te presenta la factura hasta que el caso no previsto aparece en producción. Cada vez que lo cierras, ocurre lo contrario: el compilador gana capacidad de vigilancia sobre todo el código presente y, lo que es más valioso, sobre todo el código futuro que aún no se ha escrito. Por eso la mejor forma de pensar en el case exhaustivo no es como una comodidad sintáctica sino como el cobro de una decisión tomada mucho antes, cuando alguien escribió type y enumeró las alternativas en lugar de dejar un String abierto. La exhaustividad es el dividendo de haber cerrado el modelo, y el comodín es la renuncia voluntaria a cobrarlo.
- Escribe una función total sobre
EstadoCargacon las cuatro ramas y comprueba que compila sin ningún comodín ni valor por defecto. - Borra una rama y lee entero el mensaje del compilador; identifica qué parte del mensaje nombra el caso que falta y por qué puede saberlo.
- Añade un quinto constructor al tipo sin tocar ninguna función y cuenta cuántos errores aparecen; comprueba que el conjunto de errores es exactamente el conjunto de sitios que había que revisar.
- Reescribe una de esas funciones con un comodín final, vuelve a añadir el constructor y observa que compila; explica qué error de comportamiento acaba de pasar desapercibido.
- Escribe un
casecuyas ramas devuelvan tipos distintos y explica, a partir del error, por qué uncasees una expresión y no una sentencia. - Compara la garantía de un
casede Elm con la de unswitchsobre cadenas de texto y con la de una jerarquía de clases abierta; señala en cada caso qué impide la comprobación. - Argumenta con un ejemplo propio por qué exhaustivo no implica correcto, y qué clase concreta de error sí queda eliminada.