wandres.dev
EL LEGADO DE ELM · lo que sembró

Más allá de la arquitectura: Svelte, los signals y los errores de Rust

La influencia de Elm que más se cita es la arquitectónica, pero no es la más profunda ni la más extendida. Esta lección recorre tres herencias que viajaron por fuera del bucle de estado y mensajes. La primera es la más documentada de todas y la menos discutida: cuando el equipo de Rust rehizo el formato de sus mensajes de error en 2016 citó explícitamente a Elm como referencia, y de ahí en adelante la calidad del diagnóstico dejó de ser un detalle de implementación para convertirse en un rasgo competitivo que hoy se le exige a cualquier compilador nuevo. La segunda es la tesis del compilador como framework, que Elm encarnaba antes de que existiera Svelte y que Svelte convirtió en argumento de venta. La tercera es la más incómoda, porque parece una refutación: los signals reparten el estado en celdas pequeñas y observables, justo lo contrario del árbol único, y sin embargo conservan intacto el núcleo del legado. Al final se propone una distinción metodológica que conviene tener a mano siempre que se hable de influencia: importación documentada, convergencia y redescubrimiento no son lo mismo, y confundirlas produce historia mala y decisiones peores.

⏱ 18 min

Preguntar por la influencia de un lenguaje suele llevar a la respuesta equivocada, porque instintivamente buscamos herederos que se le parezcan y la herencia real casi nunca funciona así. Elm tuvo descendencia arquitectónica evidente en Redux, en The Composable Architecture y en el linaje de MVI, y esa parte de la historia se cuenta sola. Pero hay una influencia más silenciosa, más difícil de rastrear y probablemente más importante, que no viajó en forma de patrón sino en forma de expectativa. Evan Czaplicki publicó en 2015 dos textos breves, uno sobre errores de compilación pensados para humanos y otro sobre la idea del compilador como asistente en lugar de como juez, y en ambos defendía una tesis que entonces parecía casi frívola: que la experiencia de equivocarse forma parte del diseño del lenguaje y merece el mismo cuidado que la semántica. Un año después, el equipo de Rust rehacía por completo el formato de sus diagnósticos y citaba a Elm entre sus referencias. Diez años después, ningún lenguaje nuevo se atreve a presentarse en público con mensajes de error crípticos. Esa transformación no dejó ninguna biblioteca ni ningún patrón de diseño, no aparece en los diagramas de arquitectura y sin embargo ha ahorrado más horas de trabajo humano que cualquier otra idea de esta lección. Conviene estudiarla junto a otras dos herencias oblicuas, la del compilador que sustituye al runtime y la de los signals, porque juntas dibujan mejor el mapa de lo que realmente se transmitió.

🎯 Al terminar esta lección sabrás
  • Reconstruir la genealogía documentada entre los mensajes de error de Elm y la reforma del diagnóstico en Rust, y explicar qué principios concretos se transfirieron.
  • Enunciar la tesis del compilador como framework y situar a Elm y a Svelte respecto a ella sin inventar una filiación que no está documentada.
  • Analizar los signals como la inversión de la premisa del estado único y determinar qué parte del legado conservan y cuál abandonan.
  • Distinguir con criterio entre importación documentada, convergencia y redescubrimiento al atribuir influencias.

El compilador como asistente

El argumento de fondo es que un mensaje de error es una interfaz de usuario y que, como toda interfaz, tiene un destinatario, un contexto de uso y un estado emocional asociado. Quien lee un error está bloqueado, suele tener poca información y necesita tres cosas en este orden: dónde está el problema, por qué el compilador cree que lo es, y qué puede hacer al respecto. Los compiladores tradicionales daban lo primero de forma aproximada, lo segundo en un vocabulario interno que describía el estado del verificador de tipos en lugar del problema del programador, y lo tercero jamás. Elm reorganizó la salida entera alrededor de esas tres preguntas: recortó el fragmento de código relevante, subrayó la región exacta, expuso el tipo esperado y el encontrado uno debajo de otro para que la diferencia se viera de un vistazo, y añadió una sugerencia redactada en lenguaje llano, a veces con un enlace a la explicación del concepto implicado.

-- TYPE MISMATCH ------------------------------------------- src/Main.elm

The 2nd argument to `map` is not what I expect:

23|   List.map String.length usuarios
                             ^^^^^^^^
This `usuarios` value is a:

    List Usuario

But `map` needs the 2nd argument to be:

    List String

Hint: Perhaps you wanted to extract the name first with a function like
`.nombre` before measuring its length.

La reforma de Rust adoptó la misma anatomía y la llevó más lejos en un punto donde Elm no había entrado. Los diagnósticos de rustc distinguen la etiqueta principal de las secundarias, numeran los errores para poder pedir una explicación extensa y, sobre todo, marcan algunas sugerencias como aplicables por máquina, de modo que una herramienta puede reescribir el código por ti. Esa última pieza cierra el círculo del argumento original: si el compilador ya sabe qué hay que hacer, obligar al humano a teclearlo es una descortesía innecesaria.

Vale la pena señalar qué hizo posible ese cuidado, porque no es solo voluntad. Un compilador puede permitirse conjeturar la intención del programador cuando el espacio de programas válidos es pequeño y la información de tipos es completa; en un lenguaje con inferencia total y sin escotillas, la distancia entre lo que el usuario escribió y lo que probablemente quería escribir suele ser de una operación, y por eso la sugerencia acierta. Donde el sistema de tipos es parcial o admite anotaciones que apagan la verificación, la misma estrategia produce sugerencias erráticas que enseñan a desconfiar del compilador. La calidad del diagnóstico, por tanto, no es una capa de barniz sobre un lenguaje cualquiera: es un dividendo del rigor que se le exigió antes.

error[E0308]: mismatched types
  --> src/main.rs:23:26
   |
23 |     let total: usize = usuarios.len() + nombre;
   |                        --------------   ^^^^^^ expected `usize`, found `String`
   |                        |
   |                        expected because this is `usize`
   |
help: consider using the length of the string instead
   |
23 |     let total: usize = usuarios.len() + nombre.len();
   |                                                ++++++
ℹ️
Qué se transfirió exactamente

No se transfirió un formato sino una jerarquía de prioridades. Primero, el error habla del programa del usuario y no de la maquinaria interna del compilador. Segundo, el fragmento se muestra en el mensaje para no obligar a saltar al editor. Tercero, la comparación entre lo esperado y lo hallado se dispone visualmente en lugar de narrarse. Cuarto, se ofrece una salida concreta. Cualquier compilador que respete esos cuatro puntos produce diagnósticos reconocibles como parientes, aunque no haya leído una línea de Elm.

El compilador como framework

La segunda herencia es más difusa y exige precisión al enunciarla. Elm compila a JavaScript con un runtime deliberadamente pequeño, y como el lenguaje no tiene reflexión ni carga dinámica de código, el compilador puede eliminar de forma agresiva todo lo que no se usa, incluidas funciones concretas dentro de un módulo importado. La consecuencia es que el tamaño del resultado depende de lo que el programa hace y no de lo que la biblioteca ofrece. Svelte defendió públicamente una tesis emparentada: que muchas de las tareas que los frameworks resuelven en tiempo de ejecución, comparando árboles y observando dependencias, se pueden resolver en tiempo de compilación analizando el código fuente, y que el framework ideal es el que desaparece del paquete final. Sería un error afirmar sin pruebas que Svelte descienda de Elm, porque su antecedente inmediato es otro y su autor lo ha explicado varias veces; lo que sí puede afirmarse es que ambos son instancias de la misma tesis y que Elm la encarnaba antes.

// Lo que el programador escribe en un componente compilado
let cuenta = 0
function incrementar() {
  cuenta += 1
}

// Lo que el compilador emite: una invalidacion puntual,
// sin arbol virtual y sin comparacion en tiempo de ejecucion
function incrementar() {
  $$invalidate(0, (cuenta += 1))
}
🧹

Eliminación por función

Sin reflexión ni evaluación dinámica, el compilador demuestra qué código es inalcanzable y lo borra. La ausencia de escotillas de escape es lo que hace posible la optimización.

📐

Formato sin opciones

La herramienta de formato de Elm nació sin configuración deliberadamente, para que el estilo dejara de ser tema de discusión. Hoy esa postura es mayoritaria en las herramientas nuevas.

🔢

Versionado comprobado

El gestor de paquetes compara las interfaces públicas de dos versiones y decide él mismo si el salto es mayor o menor. Sigue siendo la idea menos imitada y la que más falta hace.

🩺

Diagnóstico como producto

La calidad de los mensajes pasó de detalle interno a criterio de evaluación pública de un lenguaje. Es la herencia más extendida y la que menos se atribuye.

Los signals: la herejía que también es herencia

La tercera herencia parece una refutación y por eso resulta instructiva. Un signal es una celda de estado que registra quién la lee, de modo que al cambiar puede avisar exactamente a los cálculos y a los fragmentos de interfaz que dependen de ella. La familia es antigua, viene de las bibliotecas de observables de hace quince años y hoy está en Solid, en Vue, en Angular, en Preact y en la versión reciente de Svelte. Su premisa es lo contrario de la de Elm: en vez de un árbol único de estado que se recorre entero en cada cambio, muchas celdas pequeñas, cada una con su grafo de dependencias, y una propagación cuyo coste depende del tamaño del cambio y no del tamaño de la aplicación. Donde Elm dice reconstruye todo y deja que el sistema descubra la diferencia, el signal dice sé exactamente quién te lee y avísale a él.

// El nucleo del mecanismo cabe en veinte lineas
let lectorActual = null

function signal(valorInicial) {
  let valor = valorInicial
  const lectores = new Set()
  return {
    get() {
      if (lectorActual) lectores.add(lectorActual)
      return valor
    },
    set(nuevo) {
      valor = nuevo
      for (const efecto of [...lectores]) efecto()
    },
  }
}

function efecto(fn) {
  const correr = () => {
    lectorActual = correr
    fn()
    lectorActual = null
  }
  correr()
}

Ese fragmento condensa la diferencia filosófica entera. La dependencia no se declara: se descubre observando quién lee durante la ejecución, lo cual exige un estado global temporal y una fase de recolección que el programador no ve. Es un mecanismo brillante y también profundamente implícito, justo la clase de magia que Elm había desterrado por principio. Quien haya depurado un ciclo de actualización inesperado en un sistema reactivo de grano fino reconocerá el precio: cuando la topología del grafo depende de qué ramas se ejecutaron esta vez, razonar sobre ella deja de ser un ejercicio de lectura.

flowchart TB
E[Elm: un estado y reconstruccion total] --> P[Vista como funcion pura del estado]
S[Signals: celdas con grafo de dependencias] --> P
E --> A[Reproduccion e historial gratis]
E --> B[Coste proporcional al arbol]
S --> C[Coste proporcional al cambio]
S --> D[Sin punto unico de auditoria]
P --> F[Herencia comun: nadie muta la interfaz a mano]
style E fill:#a6e3a1,color:#11111b
style S fill:#89b4fa,color:#11111b
style F fill:#cba6f7,color:#11111b

Y sin embargo la herencia está intacta en lo esencial. En un sistema de signals nadie escribe instrucciones para modificar la pantalla: se declara cómo se deriva lo visible de lo almacenado y el sistema se encarga de mantener la correspondencia. Los valores derivados son funciones puras de otros valores. La invalidación manual, que era la fuente principal de errores en la programación de interfaces, ha desaparecido igual que desapareció en Elm, solo que por otro mecanismo. Lo que se cede a cambio es la propiedad que hacía posible el viaje en el tiempo: sin un valor único que contenga todo, no hay nada que guardar, comparar ni reproducir, y la depuración vuelve a ser local. Lo interesante del momento presente es que las arquitecturas más maduras están combinando ambas cosas, con un estado inmutable y único en la cima para poder auditarlo y observación de grano fino por debajo para que la propagación sea barata, que es exactamente lo que hizo The Composable Architecture al adoptar observación en su capa de vista.

Influencia no es parecido: distinguir importación, convergencia y redescubrimiento

Al escribir historia técnica se comete casi siempre el mismo error, y conviene desactivarlo porque tiene consecuencias prácticas inmediatas. El error consiste en tratar el parecido como prueba de descendencia. Existen al menos tres relaciones distintas entre dos sistemas que se parecen, y solo una de ellas es influencia en sentido estricto. La primera es la importación documentada: alguien leyó, entendió y adaptó, y lo dijo, como ocurre con la reforma del diagnóstico en Rust o con el puerto de la arquitectura que hizo Spotify para Android. La segunda es la convergencia: dos equipos llegan a la misma figura porque el problema tiene pocas soluciones buenas, como ocurrió con el bucle unidireccional en tres plataformas que apenas se hablaban. La tercera es el redescubrimiento, que es la convergencia con amnesia: la solución ya existía, se olvidó y vuelve a inventarse con otro nombre, que es lo que ha pasado varias veces con la reactividad de grano fino desde los observables de hace quince años. Las tres producen sistemas parecidos y ninguna de las tres autoriza a decir lo mismo. La razón por la que esto importa fuera de la erudición es que cada relación implica una lección distinta cuando llega la hora de decidir. Si es importación, se puede ir a la fuente y estudiar qué problemas encontró el original, porque los vas a encontrar tú. Si es convergencia, tienes evidencia fuerte de que la figura está determinada por el problema y de que apartarse de ella te costará caro, lo cual es una información valiosísima. Si es redescubrimiento, lo que tienes delante es una advertencia sobre la memoria colectiva de tu propio oficio y una invitación a ir a leer lo viejo antes de escribir lo nuevo. Aplicar esta distinción a la propia genealogía de Elm produce una imagen mucho más honesta y también más útil que la lista de descendientes: unas cosas se importaron, otras se reinventaron y otras estaban en la teoría de lenguajes desde los años setenta, y Elm fue el vehículo que las trajo a un sitio donde por fin importaban.

⚔️ Rastrear una herencia sin dar nada por supuesto
  1. Escoge tres compiladores que uses y clasifica sus mensajes de error según los cuatro criterios del recuadro; puntúa cada uno y justifica la nota.
  2. Toma un error críptico real de tu día a día y reescríbelo entero siguiendo esa anatomía, incluida una sugerencia que una herramienta pudiera aplicar sola.
  3. Busca la documentación pública donde un proyecto declare su influencia y clasifica la relación como importación, convergencia o redescubrimiento, aportando la cita.
  4. Implementa un signal mínimo con lectura registrada y notificación, y comprueba en qué punto necesitarías un mecanismo de limpieza de dependencias obsoletas.
  5. Diseña sobre el papel un sistema con estado inmutable en la cima y observación de grano fino debajo, y enumera qué garantías conserva cada mitad.
  6. Argumenta en contra de esta lección: sostén que la reactividad de grano fino no debe nada a Elm y que la coincidencia en la vista declarativa es demasiado genérica para llamarse herencia.