wandres.dev
LA FILOSOFÍA · cero errores en runtime

Llevarse la filosofía sin llevarse el lenguaje

La mayoría de quienes estudian Elm no escribirán con él su próximo producto, y sin embargo casi todos salen del estudio programando distinto en el lenguaje que ya usaban. Esta lección convierte ese efecto difuso en un programa concreto de traslado: identifica qué partes de la filosofía viajan intactas a TypeScript, Rust o Swift, cuáles llegan debilitadas y cuáles no llegan en absoluto porque dependen de que el lenguaje entero coopere. Después detalla la disciplina que sustituye al compilador cuando el compilador no obliga: dónde ponerla, cómo automatizarla con la configuración y el análisis estático, y por qué una regla de equipo que no está mecanizada dura exactamente hasta la primera entrega urgente. Cierra con la pregunta que importa: qué clase de garantía queda cuando la garantía deja de ser del lenguaje.

⏱ 18 min

Existe un patrón que se repite con una regularidad casi cómica entre quienes pasan una temporada con Elm y luego vuelven a su lenguaje habitual: no lo abandonan, pero empiezan a escribirlo con acento. Modelan el estado con uniones etiquetadas donde antes ponían tres banderas booleanas independientes, devuelven el fallo en vez de lanzarlo, validan el dato en la frontera en lugar de comprobarlo cinco veces por dentro, y desarrollan una alergia repentina a los valores que pueden faltar sin que el tipo lo diga. Ese efecto es la aportación más medible de Elm a la industria, mucho más amplia que su cuota de uso, y no se explica porque enseñe técnicas nuevas —casi todas existían antes en la tradición funcional tipada—, sino porque las lleva al extremo y no ofrece alternativa, de modo que el aprendizaje no queda como una preferencia estilística sino como un hábito. La pregunta práctica es qué parte de ese hábito sobrevive al regreso a un lenguaje permisivo, y la respuesta depende menos del lenguaje de destino de lo que suele suponerse y mucho más de dónde se coloque el punto de obligación.

🎯 Al terminar esta lección sabrás
  • Separar los principios que viajan intactos, los que llegan debilitados y los que no son transferibles.
  • Traducir cada principio a la forma idiomática de TypeScript, Rust y Swift sin caer en la imitación literal.
  • Diseñar la disciplina mecanizada que sustituye al compilador cuando el lenguaje no obliga.
  • Evaluar qué clase de garantía queda cuando la propiedad deja de ser del lenguaje y pasa a ser del proyecto.

Los principios y su equipaje

El primero y más rentable es hacer imposibles los estados inválidos. En Elm se expresa con un tipo unión cuyos constructores llevan justo los datos que ese estado necesita, de modo que no existe la combinación de un cargando verdadero con unos datos ya presentes. Viaja completo a Rust y a Swift, cuyos tipos enumerados con valores asociados son el mismo mecanismo con otro nombre, y viaja bastante bien a TypeScript mediante uniones discriminadas por una etiqueta literal. El segundo es tratar la ausencia y el fallo como valores en vez de como sucesos: Option y Result en Rust, el opcional y el tipo de resultado en Swift, y en TypeScript la unión con null bajo comprobación estricta más un resultado explícito en lugar de lanzar. El tercero es la exhaustividad, que Rust y Swift imponen en su construcción de selección y que TypeScript solo consigue con un chequeo auxiliar sobre el tipo never.

-- El principio en su forma original: el estado imposible no se escribe
type EstadoRemoto error dato
    = NoPedido
    | Cargando
    | Fallo error
    | Cargado dato


-- No existe la combinacion cargando mas datos presentes,
-- ni la de fallo mas datos presentes, porque no hay como escribirla.
ver : EstadoRemoto String Int -> String
ver estado =
    case estado of
        NoPedido ->
            "sin pedir"

        Cargando ->
            "cargando"

        Fallo mensaje ->
            "error: " ++ mensaje

        Cargado n ->
            String.fromInt n
🧩

Estados imposibles

Uniones con datos dentro. Enumerados con valores asociados en Rust y Swift, uniones discriminadas en TypeScript.

📭

Ausencia y fallo como valor

Nada de excepciones para el flujo esperado. Option, Result, opcionales y resultados explícitos en la firma.

Exhaustividad

Cubrir todos los casos por obligación, no por costumbre. En TypeScript exige el chequeo auxiliar sobre never.

🛂

Frontera validada

El dato externo se convierte en tipo del dominio al entrar, una sola vez, en un único punto declarado.

Lo que llega debilitado y lo que no llega

La frontera validada es el principio que más rendimiento da y el que peor viaja, no por falta de herramientas sino porque nada obliga a usarlas. Cualquiera de los tres lenguajes permite convertir el dato externo en un valor del dominio en el punto de entrada, con las bibliotecas de esquema de TypeScript, con la serialización dirigida por tipos de Rust o con los protocolos de codificación de Swift. La diferencia es que en Elm no hay otra manera de entrar, y fuera de Elm siempre queda el atajo de afirmar el tipo sin comprobarlo. Lo mismo ocurre con la pureza: puedes escribir un núcleo de funciones sin efectos y empujar la entrada y la salida a un borde delgado, pero ningún compilador te avisará si un día alguien escribe una lectura de reloj en medio de la lógica.

ℹ️
Traducir no es imitar

El error típico del converso es reproducir la forma superficial en vez del principio. Reimplementar el bucle completo de mensajes en un lenguaje con su propio idioma de estado suele producir capas de ceremonia que nadie del equipo entiende, y encadenar mapeos y aplanados en Rust cuando el operador de interrogación resuelve lo mismo en una línea es escribir Elm con sintaxis prestada. El principio que viaja es la propiedad —el estado inválido no se puede escribir, el fallo aparece en la firma, el caso nuevo rompe la compilación—, no la notación con la que Elm la consigue. Cada lenguaje tiene su forma idiomática de obtener esa propiedad, y usarla es lo que hace que el equipo acepte el cambio.

-- Cuarto principio: el dato externo se convierte en dominio al entrar
type Correo
    = Correo String


-- El unico constructor publico valida. Fuera del modulo
-- no hay forma de fabricar un Correo que no haya pasado por aqui.
desdeTexto : String -> Result String Correo
desdeTexto texto =
    if String.contains "@" texto then
        Ok (Correo (String.toLower (String.trim texto)))

    else
        Err "formato invalido"


aTexto : Correo -> String
aTexto (Correo valor) =
    valor

Ese patrón es el que mejor sobrevive fuera de Elm, y conviene entender por qué. No depende de que el lenguaje prohíba nada, sino de que la única puerta de construcción esté cerrada por el sistema de módulos: un constructor privado, una función pública que valida y un tipo que a partir de ahí significa dato correcto. TypeScript lo consigue con un tipo marcado y un constructor exportado en solitario, Rust con un campo privado dentro de su módulo y Swift con un inicializador que puede fallar y un almacenamiento privado. En los tres casos, quien escriba código nuevo dentro de seis meses no necesita conocer ninguna norma del equipo: la única forma de obtener el valor es la correcta, porque no hay otra. Esa es exactamente la estrategia de Elm aplicada al nivel del módulo en vez del lenguaje, y es la única que no se erosiona con la rotación de personas.

Y hay una parte que sencillamente no se puede llevar: la garantía global. En Elm, la ausencia de excepciones es una propiedad del lenguaje que se cumple aunque tu equipo sea descuidado y aunque tus dependencias las escribieran desconocidos. En cualquier otro sitio, lo máximo alcanzable es una propiedad local del código que tú controlas, sostenida por configuración y revisión, y que se detiene en el borde de la primera biblioteca ajena. Esa diferencia no es un matiz: es la razón por la que un proyecto disciplinado en un lenguaje permisivo se parece a Elm en los buenos días y no se le parece en los malos.

La disciplina mecanizada

Una regla de equipo que solo vive en la cabeza de las personas dura hasta la primera entrega con prisa, así que la única disciplina que cuenta es la que está escrita en la configuración y falla la construcción. En TypeScript eso significa activar el modo estricto completo, prohibir el tipo comodín con una regla que rompa el análisis en vez de avisar, prohibir la afirmación de tipo sin comprobación y exigir el chequeo exhaustivo en las uniones. En Rust significa prohibir el bloque inseguro en el archivo raíz salvo excepción documentada, y tratar como error los avisos que señalan resultados ignorados. En Swift significa vetar el desempaquetado forzoso mediante análisis estático y aislar los pocos puntos donde se acepta.

flowchart LR
P[Principio de Elm] --> T[Traduccion idiomatica]
T --> M[Mecanizada en la construccion]
T --> N[Solo acordada en el equipo]
M --> G[Propiedad local fiable]
N --> O[Se pierde en la primera urgencia]
G --> L[Limite: dependencias ajenas]
style M fill:#a6e3a1,color:#11111b
style N fill:#f38ba8,color:#11111b
style L fill:#f9e2af,color:#11111b
🔧

Configuración estricta

Modo estricto completo y comprobación de nulos activada. Sin eso, ninguno de los demás principios se sostiene.

Escotillas vetadas

Prohibir el tipo comodín, la afirmación sin comprobación, el bloque inseguro y el desempaquetado forzoso.

🧱

Construcción que falla

La regla que solo avisa no es una regla. Debe romper la integración continua o no existe.

🔐

Constructor único

Poner la obligación en el tipo, no en la norma escrita: si solo hay un camino, no hace falta recordarlo.

Hay un segundo nivel de mecanización que se pasa por alto y que es el más rentable en equipos grandes: colocar la obligación en el tipo y no en la regla. Si el único constructor público de un identificador de usuario es una función que valida, ningún compañero podrá fabricar uno inválido aunque nadie revise su cambio, y no hará falta ninguna norma escrita. Ese patrón —tipos que solo pueden construirse por el camino correcto— es la forma en que las ideas de Elm sobreviven mejor fuera de Elm, porque no dependen de que nadie recuerde nada.

La garantía que queda

Al final del traslado conviene enunciar con precisión qué se puede afirmar, porque exagerarlo arruina la credibilidad del que lo intenta. Lo alcanzable en un lenguaje permisivo con configuración estricta y buen diseño de tipos es una propiedad de esta forma: dentro del código que controlamos, los estados inválidos no son representables, los fallos aparecen en las firmas, el dato externo se valida en un único punto y añadir un caso a una unión rompe la compilación donde debe. Eso es mucho, cubre la mayor parte de los fallos que ese equipo produciría, y se detiene exactamente en el borde de la primera dependencia ajena y en cualquier archivo donde alguien haya desactivado una regla con un comentario.

La honestidad sobre ese límite es lo que separa la práctica madura del entusiasmo del converso. Quien afirma que su proyecto en un lenguaje permisivo tiene las garantías de Elm no ha entendido la diferencia entre una propiedad construida y una propiedad vigilada, y perderá la discusión en cuanto alguien encuentre el primer contraejemplo en una biblioteca de terceros. Quien afirma que ha reducido drásticamente una clase de fallos en la parte del sistema donde tiene autoridad está diciendo algo verdadero, defendible con datos y suficiente para justificar el esfuerzo ante cualquier persona razonable.

Lo que de verdad se lleva uno no es una técnica, es un cambio en la pregunta

Al final del recorrido conviene decir qué es lo que realmente cambia, porque no es la lista de patrones. Antes de trabajar así, la pregunta que uno le hace a su propio código mientras lo lee es defensiva: puede esto ser nulo aquí, capturará alguien esta excepción más arriba, cubre este condicional el caso que añadimos el mes pasado. Es una pregunta que se repite en cada función, que nunca se responde del todo y que consume una fracción fija y silenciosa de la capacidad de pensar. Después de trabajar así, esa pregunta desaparece del primer plano porque la respuesta está en el tipo, y el hueco lo ocupa otra muy distinta: describe esta estructura el dominio tal como es. La segunda pregunta es la única que produce software correcto, porque los fallos que sobreviven a un buen sistema de tipos son casi siempre malentendidos sobre el problema, no descuidos sobre el código. Y aquí está el motivo por el que merece la pena estudiar Elm aunque no vayas a usarlo: te obliga a habitar durante un tiempo un entorno donde la primera pregunta es literalmente inexpresable, y esa experiencia recalibra el juicio de una manera que ninguna lectura consigue. Cuando vuelves a un lenguaje permisivo, sigues sin poder eliminar la primera pregunta —tu lenguaje no te lo permite—, pero ya sabes cuánto te costaba, sabes exactamente qué construcciones la generan y dedicas tu presupuesto de disciplina a acorralarla en el sitio donde más daño hacía. Eso no es adoptar Elm; es haber aprendido, con un experimento controlado y ajeno, cuánto de lo que llamábamos complejidad esencial de nuestro trabajo era en realidad el ruido de fondo de unas decisiones de diseño que ni siquiera habíamos tomado nosotros.

⚔️ Traslada la filosofía a tu lenguaje
  1. Elige un estado de tu aplicación modelado con banderas independientes y sustitúyelo por una unión etiquetada; cuenta cuántas combinaciones imposibles desaparecen.
  2. Convierte una función que lanza en una que devuelve el fallo en su firma y observa cómo se propaga la obligación por quien la llama.
  3. Implementa el chequeo exhaustivo en una unión de TypeScript y comprueba que añadir un caso nuevo rompe la compilación en todos los puntos correctos.
  4. Localiza la frontera por la que entran datos externos a tu proyecto y comprueba si el tipo se afirma o se valida; corrige un caso.
  5. Mecaniza en la configuración una sola regla que hoy sea un acuerdo verbal del equipo y mide cuántas violaciones aparecen.
  6. Escribe qué garantía puedes afirmar honestamente sobre tu proyecto después de estos cambios y en qué punto exacto deja de ser cierta.