wandres.dev
MÁQUINAS FINITAS · estados y transiciones

La tabla de transiciones

La función de transición delta no es una abstracción etérea: tiene una representación tabular exacta. La tabla de transiciones coloca los estados en las filas, los eventos en las columnas, y en cada celda el estado destino. Esa matriz ES delta, escrita de forma que se puede auditar de un vistazo: cada celda vacía es un evento que ese estado ignora, y cada hueco sospechoso salta a la vista antes de escribir una línea de código. Esta lección presenta la tabla como la representación formal de la máquina, isomorfa al diagrama y a la configuración de objeto.

⏱ 16 min

Un diagrama de estados es hermoso para intuir el flujo, pero traicionero para verificar que está completo: los huecos no se ven, porque una transición que falta es, literalmente, una flecha que no dibujaste. La tabla de transiciones cura esa ceguera. Al disponer todos los estados contra todos los eventos en una rejilla, cada combinación posible ocupa una casilla, y las casillas vacías —los eventos que un estado no maneja— dejan de esconderse: gritan desde la cuadrícula. La tabla no es un accesorio didáctico; es la forma canónica de la función de transición, y la herramienta con la que se audita una máquina antes de programarla.

🎯 Al terminar esta lección sabrás
  • Entender la tabla de transiciones como la representación tabular exacta de la función δ.
  • Leer filas como estados, columnas como eventos y celdas como el estado destino.
  • Distinguir una función de transición total de una parcial, y qué significa una celda vacía.
  • Reconocer las tres representaciones isomorfas: tabla, diagrama y configuración de objeto.

δ como matriz: filas, columnas, celdas

Recuerda la firma de la función de transición: δ: Q × Σ → Q. El producto cartesiano Q × Σ es, ni más ni menos, el conjunto de todas las casillas de una tabla que cruza estados con eventos. Poner los estados en las filas y los eventos en las columnas, y escribir en cada celda el estado destino, no es una analogía: es δ tabulada. Para la máquina de petición del nivel anterior, la tabla se lee así.

Estado \ Evento FETCH RESOLVE REJECT RETRY
idle loading
loading success error
success
error loading

Cada celda con contenido es una transición declarada; cada guion es una casilla vacía. Léela por filas y obtienes, para cada estado, exactamente qué eventos lo mueven y a dónde. Léela por columnas y obtienes, para cada evento, en qué estados tiene efecto. Esa doble lectura es la que un diagrama no te da de forma sistemática, y es oro puro para revisar cobertura.

Quien venga de la teoría de grafos reconocerá la figura: si numeras los estados, la tabla es una matriz de adyacencia etiquetada por eventos, y la máquina es un grafo dirigido donde los nodos son estados y las aristas, transiciones con etiqueta. Esa dualidad grafo-matriz es la misma que gobernaba el DAG reactivo del nivel 8, y arrastra las mismas preguntas de alcanzabilidad: ¿se llega a cada estado desde el inicial? ¿hay estados sumidero de los que ninguna transición sale? Un estado inalcanzable es código muerto que jamás se ejecutará; un sumidero no previsto es una trampa donde la interfaz se queda encallada sin salida. Ambas patologías se detectan por inspección de filas y columnas antes de ejecutar una sola línea.

Hay una propiedad más que la tabla vuelve evidente: el determinismo. Como cada casilla contiene a lo sumo un destino, para cada par estado-evento hay una única salida. Si alguna vez te descubres queriendo escribir dos destinos en una misma celda, la tabla te está avisando de algo: o te falta un estado que distinga los dos casos, o necesitas una guarda que elija entre ellos según una condición. La rejilla no solo audita la cobertura; obliga a que la máquina siga siendo determinista, porque una celda con dos valores sencillamente no cabe.

Total o parcial: qué significa una celda vacía

Aquí aparece una decisión de diseño con consecuencias profundas. Una función de transición es total si toda casilla está definida: para cada par estado-evento existe un destino, aunque sea el propio estado. Es parcial si se permiten celdas vacías. La diferencia no es cosmética.

💡
La casilla vacía es una decisión, no un olvido

En una máquina parcial, una celda vacía significa “este evento no aplica en este estado”. Cuando llega igualmente —y en una interfaz real llega: un clic tardío, un mensaje duplicado, una condición de carrera—, la máquina debe hacer algo definido. Las dos respuestas sensatas son ignorarlo silenciosamente, tratando el evento como un no-op que devuelve el mismo estado, o registrarlo como anomalía para depurar. Lo que jamás debe pasar es que la ausencia de transición provoque un estado indefinido o un fallo. Convertir una parcial en total rellenando cada hueco con un bucle sobre sí mismo hace explícita esa política: la máquina declara, casilla por casilla, que ese evento no la mueve.

Verás que la mayoría de los motores de 2026 —XState v5, Zag.js, Robot— adoptan por defecto la semántica parcial con ignorado silencioso: un evento sin transición desde el estado actual simplemente no hace nada. Es una elección deliberada de robustez, la misma filosofía que hace que el torniquete no explote cuando empujas una barra bloqueada. La tabla te obliga a mirar cada hueco y decidir a conciencia si ese silencio es correcto.

Un caso real fija por qué importa. Un usuario pulsa Guardar —pasas a enviando— y, antes de que llegue la respuesta, vuelve a pulsar. En una tabla donde la casilla enviando + SUBMIT es un bucle sobre sí misma, el segundo clic no hace nada: ni lanza una segunda petición ni corrompe el estado. En una implementación sin tabla, ese segundo clic suele disparar una petición duplicada, el clásico doble cobro. La celda que decidiste a conciencia es, literalmente, la defensa contra una condición de carrera que de otro modo acabaría en producción.

📊

Total: sin huecos

Cada par estado-evento tiene destino explícito. Máxima claridad, más ceremonia. Nada queda a la interpretación.

🕳️

Parcial: huecos con política

Se permiten celdas vacías con una regla clara: ignorar o registrar. El evento inaplicable no rompe nada.

🔍

Auditar cobertura

La rejilla revela de un vistazo qué combinaciones no contemplaste. Ningún hueco se esconde como en un diagrama.

Tres representaciones, una sola máquina

La tabla, el diagrama y la configuración de objeto son isomorfos: describen la misma δ con notaciones distintas, y se traducen mecánicamente entre sí. El diagrama gana en intuición de flujo; la tabla, en verificación de completitud; el objeto, en ser ejecutable directamente. La configuración anidada que usan los motores no es más que la tabla escrita como estructura de datos, con cada estado como clave y sus eventos como subclaves.

const tabla = {
  idle:    { FETCH: "loading" },
  loading: { RESOLVE: "success", REJECT: "error" },
  success: {},
  error:   { RETRY: "loading" },
} as const

// leer una celda = aplicar delta
function delta(estado: string, evento: string): string | undefined {
  return (tabla as any)[estado]?.[evento]   // undefined = celda vacia
}

Fíjate en el undefined: es la celda vacía hecha código. La operación tabla[estado]?.[evento] es exactamente “busca la fila del estado, luego la columna del evento”; si cualquiera falta, no hay transición. Toda la máquina cabe en ese objeto, y el mismo objeto se puede volcar a un diagrama para visualizar o recorrer para generar la matriz. Esta triple equivalencia es la razón por la que herramientas como el inspector de XState o el editor visual de Stately pueden ir y venir entre diagrama y código sin pérdida: hay una sola verdad formal debajo.

📝
Tablas de Mealy y de Moore

La tabla básica solo guarda destinos, pero una máquina con salidas necesita registrar también qué acción ejecuta. En una tabla de Mealy, cada celda pasa a ser un par destino-acción: la acción va pegada a la transición, porque depende del estado y del evento juntos. En una de Moore, la acción no cabe en la celda; se anota en una columna aparte asociada al estado destino, porque depende solo de a dónde llegas, nunca de por dónde viniste. Decidir dónde colgar el efecto —en la celda o en la fila— es exactamente la elección Mealy contra Moore del nivel 9.1, ahora visible como una diferencia en la propia forma de la tabla.

Concretémoslo. La misma máquina de petición, ahora como tabla de Mealy que anota la acción de cada transición, se lee así.

Estado \ Evento FETCH RESOLVE REJECT RETRY
idle loading / lanzarPeticion
loading success / guardarDatos error / registrarFallo
success
error loading / lanzarPeticion

Cada celda no vacía es ahora un par destino / acción: al ir de idle a loading por FETCH, se ejecuta lanzarPeticion. Compara con el estilo Moore, donde lanzarPeticion colgaría del estado loading en una columna aparte y se dispararía en cada entrada, viniera de idle o de un reintento desde error. La tabla vuelve visible una decisión de diseño que en el código quedaría enterrada en el orden de las líneas.

Y como el objeto es dato, auditarlo es programable: recorrer todos los estados contra todos los eventos y listar las casillas vacías es un bucle de pocas líneas que te da la cobertura antes de confiar en la máquina.

const eventos = ["FETCH", "RESOLVE", "REJECT", "RETRY"] as const
for (const estado of Object.keys(tabla)) {
  for (const evento of eventos) {
    if (delta(estado, evento) === undefined) {
      console.log(`hueco: ${estado} + ${evento}`)   // casilla vacia sin decidir
    }
  }
}

Ejecutarlo imprime cada combinación sin transición. Revisar esa lista es revisar, una por una, si el silencio en esa casilla fue una decisión consciente o un olvido. La tabla deja de ser un dibujo y se convierte en un test de completitud que corre en integración continua: si un refactor abre un hueco nuevo, el bucle lo delata.

stateDiagram-v2
[*] --> idle
idle --> loading : FETCH
loading --> success : RESOLVE
loading --> error : REJECT
error --> loading : RETRY

Compara este diagrama con la tabla del inicio: contienen la misma información. Pero pregúntate “¿qué hace success cuando llega FETCH?” y verás la asimetría. En el diagrama buscas una flecha que sale de success y no la encuentras, lo que podría ser una decisión o un olvido —el diagrama no distingue—. En la tabla, la fila de success está entera de guiones: la ausencia es visible y, por tanto, revisable. Esa es la ventaja que justifica pensar la máquina en forma de matriz aunque luego la dibujes.

Esta equivalencia entre representaciones no es teórica: es la infraestructura sobre la que se apoyan las herramientas modernas. El editor visual de Stately toma un diagrama que dibujas y emite el objeto de configuración de XState; el inspector toma la máquina en ejecución y la vuelve a pintar como diagrama en vivo; y SCXML define un formato de intercambio en XML para que la misma máquina viaje entre herramientas de distintos fabricantes. Las tres vistas son proyecciones de una única δ, y por eso ninguna es más verdadera que otra: elige en cada momento la que mejor responda la pregunta que tienes delante —flujo, completitud o ejecución—.

La tabla es la especificación; el código es su traducción

El error de método más caro al construir una máquina de estados es empezar por el código. Se escribe el switch, se prueban los caminos felices, y las celdas vacías quedan sin pensar hasta que un evento inesperado las encuentra en producción. La tabla invierte el orden: es una especificación completa que se audita ANTES de programar. Dibujas la rejilla de todos los estados contra todos los eventos, y cada casilla te interpela con una pregunta que no puedes esquivar: si llega este evento estando en este estado, ¿qué pasa? La respuesta puede ser un destino, un bucle sobre sí mismo o un hueco deliberado, pero tiene que ser una respuesta consciente para cada una de las estados × eventos casillas. Ese acto de rellenar la matriz es diseño en su forma más pura: enfrentarte a las combinaciones que tu intuición había pasado por alto. Cuando la tabla está completa y revisada, el código es una transcripción casi mecánica —un objeto de transiciones, un switch— y los bugs de “no contemplé que llegara X estando en Y” desaparecen, porque X-en-Y era una celda que ya miraste a los ojos. La máquina no la definen las flechas bonitas del diagrama; la define la rejilla exhaustiva que te forzó a decidir sobre cada esquina del producto cartesiano.

⚔️ Rellena la matriz completa
  1. Toma la máquina de un reproductor de vídeo —parado, reproduciendo, pausado, buffering— y dibuja la tabla completa contra sus eventos.
  2. Marca cada celda vacía y decide, una por una, si el evento se ignora o se registra como anomalía. No dejes ningún guion sin política.
  3. Convierte tu tabla en el objeto de transiciones as const y escribe la función delta que la consulta.
  4. Vuelca el mismo objeto a un diagrama stateDiagram-v2 y verifica que contiene exactamente la misma información que la tabla.
  5. Cuenta las casillas: estados × eventos. Si el producto es incómodamente grande, anota esa sensación: es el preludio de la explosión que resuelve el nivel 9.5.