wandres.dev
NIVEL DIOS: SÍNTESIS · pensar en máquinas

El modelo mental completo: de la quíntupla al actor

Cuatro capas —el autómata finito con su quíntupla, el statechart de Harel, el intérprete que introduce el tiempo y el actor con identidad y buzón— parecen cuatro temas distintos y son cuatro vistas del mismo objeto. Esta lección las apila en una sola imagen, separa con precisión qué capa añade únicamente concisión y cuál añade poder de cómputo real, traduce un mismo comportamiento entre las cuatro notaciones sin perder información por el camino, y aísla el invariante que ninguna de ellas puede tocar sin dejar de ser lo que es: la transición como función total y pura de un par a un estado.

⏱ 21 min

La sensación de haber aprendido cuatro temas distintos es un artefacto del orden en que se enseñan. El autómata finito llegó con letras griegas y demostraciones; el statechart llegó con cajas anidadas y líneas discontinuas; XState llegó con un objeto de configuración y una API; el actor llegó con buzones, referencias y ciclo de vida. Cuatro vocabularios, cuatro comunidades, cuatro décadas. Pero si desmontas cada uno hasta el hueso encuentras siempre la misma pieza: un conjunto de situaciones mutuamente excluyentes y una regla que, dada una situación y algo que ocurre, dice cuál es la situación siguiente. Todo lo demás —la jerarquía, las regiones, el contexto, el buzón, la supervisión— son respuestas a preguntas de ingeniería que se apilan sobre esa pieza sin sustituirla nunca. Esta lección apila las cuatro capas en una sola imagen y, sobre todo, dice con exactitud qué gana y qué pierde el modelo al subir cada escalón.

🎯 Al terminar esta lección sabrás
  • Ver las cuatro capas —autómata, statechart, intérprete y actor— como vistas de un único objeto y no como temas independientes.
  • Distinguir con rigor qué capa añade solo concisión y cuál cambia la clase de cómputo del modelo.
  • Traducir un mismo comportamiento entre las cuatro notaciones comprobando qué se conserva en cada salto.
  • Reconocer el invariante que sobrevive a todas las capas y usarlo como piedra de toque de cualquier diseño.

Cuatro capas sobre un mismo objeto

La capa cero es el autómata finito. Un conjunto Q de situaciones, un alfabeto Σ de entradas, una función δ: Q × Σ → Q, un estado inicial q0 y un subconjunto de aceptación F. Es un objeto estático: no se ejecuta, no tiene tiempo, no tiene efectos. Su única virtud, y es enorme, es que casi todo lo que querrías preguntarle es decidible: si dos autómatas reconocen el mismo lenguaje, si un estado es alcanzable, cuál es el autómata mínimo equivalente.

La capa uno es el statechart. David Harel lo resumió en 1987 con una ecuación que conviene memorizar tal cual: statecharts es igual a diagramas de estado más profundidad más productos ortogonales más comunicación por difusión. La profundidad es la jerarquía, que permite factorizar transiciones comunes en un antepasado; el producto ortogonal son las regiones paralelas, que permiten describir dimensiones independientes sin multiplicarlas a mano; la difusión es que un evento llega a todas las regiones a la vez. Nada de esto es un modelo de cómputo nuevo: es una notación.

La capa dos es el intérprete. Aquí entra el tiempo, que en las dos capas anteriores no existía. El intérprete mantiene una cola de eventos, distingue el microstep —una transición atómica— del macrostep —la secuencia de microsteps hasta llegar a una configuración estable—, decide el orden de las acciones de salida y de entrada, y ejecuta lo que el modelo solo describía. La quíntupla es el programa; el intérprete es la ejecución.

La capa tres es el actor. Añade tres cosas que ninguna capa inferior tiene: identidad —una referencia con la que otros pueden dirigirse a él—, aislamiento —nadie toca su estado, solo le envían mensajes— y ciclo de vida —nace, puede engendrar hijos, puede morir y su muerte es observable—. Un actor no es una máquina más grande: es una máquina con dirección postal.

🔢

Autómata · qué es legal

Inventario de situaciones y una regla que las conecta. Sin tiempo, sin efectos y sin memoria fuera de Q. Todo lo interesante es decidible aquí.

🪆

Statechart · cómo se escribe sin morir

Profundidad, productos ortogonales y difusión. La misma clase de comportamiento con una fracción de los símbolos y de las aristas.

⏱️

Intérprete · cuándo ocurre

Cola de eventos, microstep y macrostep, orden de salidas y entradas. Aquí el modelo deja de describir y empieza a suceder.

📮

Actor · quién es y con quién habla

Referencia, buzón, aislamiento y muerte observable. La unidad que permite componer sistemas en lugar de agrandar máquinas.

Hay una asimetría entre las capas que conviene tener presente porque explica muchas discusiones de equipo: las dos primeras son descriptivas y las dos últimas son operacionales. Un statechart es verdadero o falso respecto a un requisito; un intérprete es correcto o incorrecto respecto a una semántica. Cuando alguien discute si un estado debería llamarse de otra forma está en la capa uno; cuando discute si una acción debe ejecutarse antes o después de otra está en la capa dos; y mezclar ambas conversaciones en la misma reunión es la causa habitual de que ninguna de las dos se cierre.

flowchart TD
A[Capa 0 automata finito: Q sigma delta q0 F] --> B[Capa 1 statechart: jerarquia regiones historia]
B --> C[Capa 2 interprete: cola de eventos microstep macrostep acciones]
C --> D[Capa 3 actor: identidad aislamiento buzon ciclo de vida]
A --> E[objeto estatico y decidible]
D --> F[sistema vivo y concurrente]
E --> G[la misma regla: par de estado y evento va a estado]
F --> G
style G fill:#a6e3a1,color:#11111b
style A fill:#89dceb,color:#11111b
style D fill:#cba6f7,color:#11111b
ℹ️
Las capas no se sustituyen, se envuelven

El error de lectura más común es creer que el statechart deroga al autómata o que el actor deroga al statechart. No ocurre nada de eso: cada capa contiene a la anterior íntegra y le añade una envoltura. Cuando depuras un actor de XState y te preguntas por qué no hizo lo que esperabas, la respuesta casi siempre está en una capa concreta —la definición era incorrecta, la jerarquía resolvía el conflicto de otro modo, el orden de microsteps no era el que suponías, o el mensaje llegó al buzón equivocado— y saber en cuál mirar primero es la mitad del trabajo de diagnóstico.

Qué añade cada capa y qué no

La pregunta que separa a quien conoce la notación de quien entiende el modelo es esta: ¿qué capas aumentan lo que se puede expresar y cuáles solo aumentan la comodidad con que se expresa? La respuesta es precisa y sorprende a mucha gente.

La jerarquía, las regiones ortogonales y la historia no añaden poder expresivo. Cualquier statechart con un número finito de configuraciones se puede aplanar a un autómata finito equivalente: la jerarquía se elimina propagando las transiciones del padre a cada hijo, y las regiones paralelas se eliminan tomando el producto cartesiano de sus estados. La equivalencia es constructiva y el aplanado es mecánico. Lo que cambia es el tamaño: dos regiones de diez estados producen cien estados planos, y tres producen mil. Eso es exactamente lo que hace valiosa la notación, y es una propiedad con nombre técnico —sucintez— que no debe confundirse con expresividad. Harel no amplió la clase de lo computable; hizo escribible lo que ya lo era.

El contexto, en cambio, sí cambia la clase. En el momento en que escribes un assign que guarda un entero sin cota y una guarda que lo compara, tu modelo dejó de ser finito: Q ya no es el conjunto de estados, sino el producto de los estados por todos los valores posibles del contexto, que puede ser infinito. Formalmente pasas a tener una máquina de estados extendida, y Minsky demostró que dos contadores sin cota más control finito bastan para simular una máquina de Turing. La palabra finita de FSM deja de ser cierta en la primera línea de contexto que escribes, y con ella se pierden las garantías de decidibilidad de la capa cero.

Los actores añaden una tercera dimensión: concurrencia y comunicación asíncrona. Aunque cada actor sea individualmente un autómata finito, un sistema de autómatas finitos que se comunican por colas sin cota tiene el problema de alcanzabilidad indecidible, resultado clásico de Brand y Zafiropulo de 1983. Es decir: la composición es estrictamente más difícil de analizar que las partes.

Capa Qué añade Poder expresivo Precio
Autómata finito nada, es la base regular ninguna estructura, explota en tamaño
Statechart jerarquía, regiones, historia idéntico, aplanable semántica de conflictos y de orden
Contexto memoria sin cota Turing-completo se pierde la decidibilidad
Intérprete tiempo, cola, efectos no aplica, es ejecución orden de acciones y reentrada
Actores identidad y mensajes alcanzabilidad indecidible entrega, orden y fallo parcial

La lectura práctica de esta tabla es una regla de diseño que vale por sí sola todo el nivel: cada fila que bajas compra comodidad con capacidad de razonar. Un modelo que se mantiene en las dos primeras filas se puede recorrer entero, aplanar, minimizar y verificar exhaustivamente. Cada campo que añades al contexto y cada actor que engendras recorta ese territorio, y hacerlo puede ser la decisión correcta —casi siempre lo es— siempre que sea una decisión y no un accidente.

⚠️
El contexto es la frontera silenciosa

Nadie anuncia que va a abandonar la clase de los lenguajes regulares. Se abandona escribiendo intentos: 0 y una guarda que comprueba si son menos de tres. Ese contador concreto es inofensivo porque está acotado y podría vivir en estados con nombre; el problema aparece cuando el contexto acumula listas, mapas y textos libres, y con ellos la certeza de que ninguna herramienta podrá enumerar el espacio de estados. Mantener acotado y pequeño el contexto no es higiene estética: es conservar la propiedad que hace verificable la máquina.

La misma máquina dicha en cuatro idiomas

Nada convence tanto como ver el mismo comportamiento escrito cuatro veces. Tomemos un cobro con un reintento: inactivo, cobrando, fallido y hecho.

// Capa 0 · la quintupla como dato. delta es total por construccion.
type Q = 'inactivo' | 'cobrando' | 'fallido' | 'hecho'
type S = 'PAGAR' | 'OK' | 'ERROR'

const delta: Record<Q, Record<S, Q>> = {
  inactivo: { PAGAR: 'cobrando', OK: 'inactivo', ERROR: 'inactivo' },
  cobrando: { PAGAR: 'cobrando', OK: 'hecho', ERROR: 'fallido' },
  fallido: { PAGAR: 'cobrando', OK: 'fallido', ERROR: 'fallido' },
  hecho: { PAGAR: 'hecho', OK: 'hecho', ERROR: 'hecho' },
}

// Capa 1 · el statechart factoriza: cobrando y fallido son hijos de activo,
// y CANCELAR se declara una sola vez en el padre en lugar de en cada hijo.

// Capa 2 · el interprete de XState. Lo mismo, mas el tiempo y los efectos.
const cobro = setup({
  actors: { cobrar },
  guards: { quedanIntentos: ({ context }) => context.intentos < 3 },
}).createMachine({
  id: 'cobro',
  initial: 'inactivo',
  context: { intentos: 0 },
  states: {
    inactivo: { on: { PAGAR: 'activo' } },
    activo: {
      on: { CANCELAR: 'inactivo' },
      initial: 'cobrando',
      states: {
        cobrando: {
          invoke: { src: 'cobrar', onDone: '#cobro.hecho', onError: 'fallido' },
        },
        fallido: {
          on: { PAGAR: { target: 'cobrando', guard: 'quedanIntentos' } },
        },
      },
    },
    hecho: { type: 'final' },
  },
})

// Capa 3 · el actor. La maquina es logica; el actor es una instancia con
// direccion, buzon y ciclo de vida observable desde fuera.
const actor = createActor(cobro).start()
actor.subscribe((s) => console.log(s.value))
actor.send({ type: 'PAGAR' })

Merece la pena observar qué se conserva y qué se transforma en cada salto. De la capa cero a la uno no se pierde ni una sola transición: solo dejan de escribirse las que el padre ya declara. De la uno a la dos aparecen cosas que en la definición no existían y que no son estados: el instante en que se ejecuta invoke, el orden entre la acción de salida del hijo y la de entrada del destino, la posibilidad de que llegue un evento mientras el efecto está en vuelo. De la dos a la tres aparece la pregunta que ninguna capa anterior podía formular: quién más puede enviarle eventos a esto, y qué ocurre cuando su padre muere.

El invariante que sobrevive a todo

Por debajo de las cuatro capas hay una sola afirmación, y es la que hay que poder recitar a las tres de la madrugada: dado un estado y un evento, el estado siguiente está completamente determinado, y calcularlo no requiere ejecutar nada. Esa función es total —para todo par hay respuesta, aunque la respuesta sea quedarse donde estás— y es pura —el mismo par produce siempre lo mismo—.

La totalidad reaparece en cada capa con otro nombre. En la capa cero es el estado sumidero que absorbe los pares sin destino. En la capa uno es la resolución de conflictos, que elige una transición cuando varias compiten y ninguna cuando ninguna aplica. En la capa dos es la política del intérprete de ignorar en silencio los eventos no manejados. En la capa tres es el buzón, que acepta cualquier mensaje aunque el actor no sepa qué hacer con él. Cuatro mecanismos distintos para no dejar jamás un par sin respuesta.

La pureza es la que paga la factura de todo lo demás. Porque la transición es pura se puede dibujar el grafo sin ejecutarlo, generar recorridos exhaustivos, probar sin montar componentes, serializar el estado y reanudarlo en otro proceso, reproducir un fallo desde un registro de eventos y comparar dos versiones del modelo. Ninguna de esas capacidades es un extra de la librería: todas son corolarios de que δ es una función.

El estado no es un valor: es una posición en un grafo, y todo lo demás es notación o ejecución

Si de este track sobrevive una sola frase en tu cabeza dentro de cinco años, que sea esta. La programación convencional trata el estado como una colección de valores —banderas, cadenas, objetos— y por eso su patología característica es la combinación imposible: el sistema entra en una configuración que nadie diseñó porque nadie enumeró las que sí. El modelado con máquinas hace la operación inversa: primero se decide el conjunto de posiciones legales, y solo después se pregunta qué datos acompañan a cada una. La consecuencia es que lo imposible deja de ser algo que hay que recordar evitar y pasa a ser algo que no se puede escribir. Y lo notable, lo que justifica haber recorrido teoría de autómatas, statecharts, un intérprete concreto y el modelo de actores, es que esa operación es la misma en las cuatro capas: cambia el vocabulario, cambia el tamaño del problema que puedes atacar, cambia lo que las herramientas pueden demostrar, pero no cambia el gesto. Por eso quien ha entendido de verdad la quíntupla puede leer un statechart complejo sin manual, y quien ha entendido el actor model reconoce en un sistema distribuido el mismo grafo que dibujaba para un desplegable. Las capas superiores existen porque las inferiores no escalan a los problemas grandes, no porque estuvieran equivocadas; y cuando algo se rompe en producción, el camino de vuelta —de actor a intérprete, de intérprete a statechart, de statechart a tabla— es siempre transitable, porque nunca dejaste de estar en el mismo objeto. La madurez en este dominio no es dominar una librería: es saber, ante cualquier problema de comportamiento, en qué capa hay que pensarlo, y tener la disciplina de no subir de capa hasta que la de abajo se haya quedado corta de verdad.

⚔️ Recorre las cuatro capas con una máquina tuya
  1. Toma una máquina de tu código y escribe su capa cero: la tabla completa de estado por evento, sin dejar ninguna celda vacía y sin usar el contexto.
  2. Cuenta cuántas filas tendría si aplanaras la jerarquía y las regiones paralelas. Ese número es la sucintez que te está regalando la notación.
  3. Recorre cada campo del contexto y marca cuáles están acotados y cuáles no. Los que no lo están son los que sacaron a tu modelo de la clase decidible.
  4. Localiza en tu máquina los cuatro mecanismos de totalidad: qué pasa con un evento no manejado en cada capa.
  5. Escribe un test que calcule una transición sin arrancar ningún actor. Si no puedes, hay lógica que se ha escapado de la función pura.
  6. Dibuja la pila de cuatro capas para tu caso y anota, en una frase por capa, qué problema concreto resuelve ahí y no antes.