Cero errores en runtime: el alcance exacto de la promesa
La frase que Elm exhibe en su portada es una afirmación técnica falsable, y como toda afirmación falsable merece que se lea con precisión de abogado antes de creerla o de despreciarla. Esta lección delimita el enunciado por sus cuatro costados: qué clase de fallo desaparece por construcción, qué residuos quedan fuera del alcance del compilador, por qué los bugs de lógica no solo sobreviven sino que en cierto sentido se vuelven más visibles, y cómo formular la promesa de modo que resista tanto al escéptico que la llama marketing como al converso que la confunde con corrección total. El objetivo no es defender ni atacar la garantía, sino aprender a manejar la distinción entre un sistema de tipos consistente y un programa correcto, que es la distinción que separa a quien entiende la propuesta de Elm de quien solo repite su eslogan.
Una promesa que no se puede refutar no dice nada, y por eso la frase de Elm es interesante: afirma algo lo bastante concreto como para que un solo contraejemplo la derribe. Ese rigor tiene un precio simétrico que casi nadie paga al citarla, porque la frase circula recortada, sin la letra que la acota, y termina significando dos cosas distintas según quién la pronuncie. En boca del escéptico se convierte en la caricatura de un lenguaje que promete programas perfectos, caricatura fácil de demoler con el primer bug de negocio que alguien encuentre. En boca del entusiasta se convierte en una licencia para no probar nada, porque si compila funciona. Ambas lecturas son falsas, y lo son por el mismo motivo: confunden la consistencia de un sistema de tipos con la corrección de un programa. Un sistema de tipos consistente garantiza que la ejecución nunca alcanza un estado que el lenguaje no sepa describir. Un programa correcto es el que hace lo que su especificación pide. Entre esas dos propiedades hay un abismo, y toda la filosofía de Elm consiste en haber decidido con una precisión inusual en qué lado de ese abismo quiere ofrecer certezas y en qué lado se limita a ayudarte.
- Enunciar la garantía en términos falsables y distinguir consistencia del sistema de tipos de corrección del programa.
- Delimitar los residuos reconocidos que el compilador no cubre y explicar por qué no invalidan el enunciado.
- Argumentar por qué los bugs de lógica sobreviven intactos y en qué sentido la garantía los deja más expuestos.
- Formular la promesa con la acotación que resiste a la vez al escéptico y al converso.
Lo que la promesa afirma
La formulación precisa no habla de programas sin fallos: habla de una clase de fallos que deja de ser expresable. La ejecución de un programa puede detenerse antes de tiempo por razones muy distintas, y Elm cierra las que nacen de una discrepancia entre lo que el código supone y lo que el valor es. Desaparece la desreferencia de un valor ausente, porque no existe un valor comodín que habite todos los tipos. Desaparece el fallo no tratado, porque no hay un canal de retorno invisible por el que un error pueda escapar. Desaparece el caso no cubierto, porque el case obliga a enumerar todos los constructores. Y desaparece la sorpresa de la frontera, porque nada externo entra sin declarar su forma. El resultado no es un programa correcto: es un programa cuya ejecución nunca alcanza un estado indefinido.
Consistencia
Si el tipo dice que hay un Int, en ejecución hay un Int. El sistema de tipos no miente nunca sobre lo que contiene un valor.
No corrección
Que el tipo sea cierto no implica que el número sea el correcto. La aritmética del negocio queda enteramente fuera del alcance.
Falsable
Un solo contraejemplo de excepción en código Elm puro refutaría la afirmación. Eso la convierte en ciencia y no en publicidad.
Acotada
El enunciado cuidadoso dice en la práctica, y admite residuos conocidos. La acotación es parte de la promesa, no una excusa.
-- Este programa compila, no lanza nada y esta mal
-- El tipo es cierto: devuelve un Int. El negocio es falso.
precioConDescuento : Int -> Int -> Int
precioConDescuento base porcentaje =
base - (base * porcentaje // 100) - 1
-- El compilador confirma que hay un Int al final.
-- Nadie confirma que ese Int sea el precio que el cliente debe pagar.
Los residuos reconocidos
Hay maneras de detener un programa Elm, y enumerarlas honestamente refuerza el enunciado en vez de debilitarlo, porque una promesa que se sabe sus propios límites es más creíble que una promesa absoluta. Debug.todo detiene la ejecución a propósito y existe para marcar trabajo pendiente durante el desarrollo. La recursión sin caso base no termina, y ningún tipo habla del tiempo. Una recursión demasiado profunda agota la pila del entorno anfitrión, que es una limitación de la máquina y no del lenguaje. La aritmética en coma flotante produce valores especiales en vez de detenerse, lo cual evita la excepción pero propaga un número sin sentido. Y el arranque desde el anfitrión puede fallar si el nodo del documento no existe.
La diferencia entre una excepción de valor ausente y un Debug.todo no es de gravedad sino de naturaleza. La primera es una discrepancia entre lo que el código supuso y lo que el valor era; nadie la pidió y nadie la vio venir. El segundo es una instrucción explícita de detener la ejecución que el programador escribió con esas mismas letras. Un lenguaje puede prometer eliminar la primera clase sin pretender eliminar la segunda, igual que un edificio antisísmico no promete resistir a un demoledor contratado. Confundirlas es el error retórico más frecuente en las críticas apresuradas al eslogan.
-- Los residuos, uno por uno
sinImplementar : Int -> Int
sinImplementar n =
Debug.todo "pendiente"
noTermina : Int -> Int
noTermina n =
noTermina (n + 1)
-- Un fallo enmascarado: no rompe, pero miente
edadDelUsuario : Int
edadDelUsuario =
Maybe.withDefault 0 (String.toInt "treinta")
El territorio que queda
El tercer ejemplo del bloque anterior es el más instructivo de los tres, porque no es un residuo del lenguaje sino una consecuencia estructural de la garantía. Al obligar a tratar la ausencia, Elm te empuja a decidir qué ocurre cuando el dato falta, y esa decisión puede ser mala. Un Maybe.withDefault 0 mal colocado convierte una excepción ruidosa en un dato silenciosamente falso que viaja por el sistema, entra en una suma, sale por un informe y termina en una factura. Ese intercambio es real y merece nombrarse: la garantía transforma fallos ruidosos en decisiones explícitas, y una decisión explícita tomada con desgana es un bug de lógica con muy buena presentación.
Cada withDefault es una afirmación sobre el dominio disfrazada de conveniencia sintáctica. Escribir cero cuando falta la edad afirma que un usuario sin edad conocida tiene cero años, lo cual es falso en todos los negocios del mundo. La alternativa correcta casi siempre consiste en propagar el Maybe hasta la frontera donde alguien tenga autoridad para decidir, o en modelar la ausencia como un estado del dominio con nombre propio. El compilador no puede distinguir un valor por defecto legítimo de una rendición, porque ambos tienen el mismo tipo.
flowchart TD P[Programa que compila] --> A[Clase eliminada por construccion] P --> B[Residuos reconocidos] P --> C[Bugs de logica] A --> A1[Valor ausente no tratado] A --> A2[Caso no cubierto] B --> B1[Todo explicito o pila agotada] C --> C1[El modelo del dominio es falso] C --> C2[El valor por defecto miente] style A fill:#a6e3a1,color:#11111b style B fill:#f9e2af,color:#11111b style C fill:#f38ba8,color:#11111b
-- La exhaustividad obliga a decidir, pero no juzga la decision
type Suscripcion
= Gratuita
| Mensual
| Anual
| Cortesia
descuento : Suscripcion -> Int
descuento suscripcion =
case suscripcion of
Gratuita ->
0
Mensual ->
10
Anual ->
20
-- Compila, cubre el caso y regala el producto entero
Cortesia ->
100
Queda además la categoría más grande de todas, la que ningún sistema de tipos de ningún lenguaje ha reducido jamás: el programa hace exactamente lo que escribiste y lo que escribiste no era lo que el negocio necesitaba. Aquí Elm no ofrece garantías, ofrece condiciones. Un modelo que hace imposibles los estados inválidos reduce el espacio en el que un malentendido puede esconderse. Un tipo unión con nombres del dominio convierte una conversación con la persona experta en una estructura revisable. La exhaustividad obliga a preguntar qué pasa en el caso raro justo cuando aún hay tiempo de preguntarlo. Nada de eso es una demostración de corrección, pero todo eso desplaza los errores hacia el momento en que salen baratos.
Cómo enunciarla sin exagerar
Promete
Que la ejecución no alcanza estados indefinidos y que ningún valor miente sobre su tipo en ningún momento.
Ayuda
A que el modelo sea revisable, a que el caso raro se discuta a tiempo y a que un refactor enumere lo que rompe.
No promete
Corrección del negocio, rendimiento, terminación de un bucle ni acierto en el diseño de la interfaz.
No sustituye
A las pruebas: las reorienta desde comprobar que no revienta hacia comprobar que acierta.
Esa reorientación de las pruebas merece un desarrollo, porque es el efecto práctico más inmediato en el día a día de un equipo. En un lenguaje permisivo, una parte considerable de la batería de pruebas existe para vigilar formas: comprobar que la función tolera un valor ausente, que no revienta con una lista vacía, que el objeto de respuesta traía los campos esperados. Todas esas pruebas desaparecen en Elm, no porque alguien decida borrarlas, sino porque el escenario que verificaban no es construible y el caso de prueba ni siquiera se puede escribir. Lo que queda es el conjunto de pruebas que interroga al dominio: si el descuento acumulado respeta el mínimo legal, si el estado de la sesión caduca cuando debe, si el orden de dos mensajes concurrentes produce el resultado correcto. Esa batería es más pequeña, más difícil de escribir y muchísimo más valiosa, y su existencia depende por completo de que alguien haya entendido el problema, que es precisamente donde la garantía del compilador termina.
La formulación defendible cabe en dos frases. Primera: en Elm, un programa que compila no lanza excepciones en tiempo de ejecución, salvo por un conjunto pequeño, conocido y documentado de residuos que el programador introduce a propósito o que pertenecen al entorno anfitrión. Segunda: esa garantía no dice absolutamente nada sobre si el programa hace lo que debe. Quien enuncia así la promesa no puede ser acusado de vender humo, porque ha declarado el alcance antes de que se lo pidan, y tampoco puede ser confundido con alguien que cree que el compilador sustituye a las pruebas.
Conviene entender qué se compra realmente con esta promesa, porque la lectura ingenua es que se compra tiempo y la lectura correcta es que se compra atención. Todo programa que trate con datos que pueden faltar y con operaciones que pueden fallar debe decidir en algún momento qué ocurre en cada uno de esos casos; no hay una tercera opción en la que la decisión no exista. Un lenguaje permisivo permite aplazarla, y lo aplazado no se evapora: reaparece como una traza de pila en producción, a una hora mala, con un usuario delante y sin ninguna información sobre qué se esperaba. Elm cobra por adelantado y a la vista. Hasta aquí la lectura habitual, que es cierta pero superficial. La lectura profunda es otra: mientras una clase entera de fallos sigue siendo posible, una parte fija de tu capacidad de razonar sobre el código se dedica permanentemente a vigilarla. Lees una función preguntándote si esto podrá ser nulo aquí, si alguien habrá capturado esta excepción más arriba, si este case cubría el caso nuevo que añadimos el mes pasado. Esa vigilancia es cara y es invisible en cualquier medición, porque no aparece en el tiempo de escribir ni en el número de fallos, sino en la profundidad con la que puedes pensar sobre el problema real. Cuando el compilador se hace cargo de esa clase, la vigilancia se libera y se puede reinvertir entera en la única pregunta que nunca podrá automatizarse: si este modelo describe fielmente el dominio. Por eso la promesa de Elm no compite con las pruebas ni las sustituye, sino que las reorienta: dejas de escribir pruebas que comprueban que el programa no se rompe y empiezas a escribir pruebas que comprueban que el programa acierta. Y ese desplazamiento, no el eslogan, es lo que cambia la manera de trabajar de quien lo experimenta una temporada.
- Escribe la formulación de la garantía en dos frases propias y sométela a un compañero escéptico; anota qué contraejemplo intenta y si cae dentro o fuera del enunciado.
- Provoca los tres residuos del ejemplo y clasifica cada uno según si lo introdujo el programador, el entorno o la no terminación.
- Busca en un proyecto tuyo tres usos de valor por defecto y decide en cada caso si es una afirmación legítima sobre el dominio o una rendición disfrazada.
- Escribe una función que compile, no lance nada y calcule mal un importe; explica qué información le faltaba al compilador para detectarlo.
- Redacta el enunciado equivalente para un lenguaje que uses a diario y compara con honestidad qué clase de fallos elimina y cuáles deja vivos.
- Defiende la tesis de que la garantía traslada trabajo en vez de ahorrarlo, y después construye el mejor argumento en contra que se te ocurra.