wandres.dev
TEORÍA DE AUTÓMATAS · la base formal

Mealy y Moore: la salida en la transición o en el estado

Un autómata que solo acepta o rechaza no basta para gobernar un sistema: hace falta que además emita algo. Las dos formas clásicas de añadir salida a una máquina finita son la de Moore, donde la salida es función únicamente del estado, y la de Mealy, donde es función del estado y del símbolo leído. Ambas son equivalentes en poder pero no en tamaño ni en temporización, y la conversión entre ellas revela un desfase de un ciclo que en hardware es célebre. Esta lección desarrolla esa equivalencia asimétrica y muestra que las acciones de entrada y salida de un statechart son Moore, las acciones colgadas de una transición son Mealy, y que confundirlas produce una familia entera de bugs reconocible.

⏱ 19 min

Los autómatas de las lecciones anteriores solo saben decir sí o no: aceptan una cadena o la rechazan. Para gobernar un sistema real hace falta algo más, que la máquina produzca salida mientras funciona, y en 1955 y 1956 aparecieron las dos respuestas que todavía usamos. George Mealy colgó la salida de las transiciones; Edward Moore la colgó de los estados. Setenta años después esa distinción sobrevive intacta en cada motor de statecharts, con otros nombres y sin que casi nadie la reconozca: cada vez que decides entre poner un efecto en entry o en la transición estás eligiendo entre Moore y Mealy. Y como los dos modelos son equivalentes en poder pero no en temporización ni en tamaño, la elección tiene consecuencias que se pagan en bugs concretos.

🎯 Al terminar esta lección sabrás
  • Definir las séxtuplas de Mealy y de Moore y precisar de qué depende la salida en cada una.
  • Convertir entre ambas y entender la asimetría: el desfase de un ciclo y el crecimiento de estados.
  • Identificar entry y exit como salida Moore y las acciones de transición como salida Mealy.
  • Aplicar un criterio para decidir dónde colocar cada efecto, incluida la gestión de recursos.

Dos séxtuplas: dónde se engancha la salida

Ambos modelos parten de la quíntupla y le añaden dos cosas: un alfabeto de salida Δ y una función de salida λ. Toda la diferencia está en la firma de λ.

En una máquina de Moore, λ: Q → Δ. La salida depende únicamente del estado en el que se está, y por tanto es una etiqueta pegada a cada nodo del grafo. Da igual por qué arista llegaste: al estar en el estado, se emite su salida. En una máquina de Mealy, λ: Q × Σ → Δ. La salida depende del estado y del símbolo que provoca la transición, y por tanto es una etiqueta pegada a cada arista. Dos aristas que llegan al mismo destino desde estados distintos, o con símbolos distintos, pueden emitir salidas distintas.

🏷️

Moore · la salida es del nodo

λ: Q → Δ. La salida es una propiedad del estado. Estable mientras permanezcas en él e idéntica vengas de donde vengas.

➡️

Mealy · la salida es de la arista

λ: Q × Σ → Δ. La salida es una consecuencia del evento concreto. Reacciona en el mismo paso en que llega el símbolo.

⚖️

Equivalentes, no intercambiables

Reconocen y traducen lo mismo, pero difieren en número de estados, en momento de emisión y en longitud de la salida.

Un ejemplo diminuto fija la diferencia mejor que cualquier definición. Un menú desplegable que enfoca su primer elemento al abrirse quiere una salida Moore: enfocar es una propiedad de estar abierto, y debe ocurrir tanto si abriste con el ratón como con el teclado. En cambio, reproducir un sonido solo cuando la apertura vino del teclado es una salida Mealy: depende del evento concreto que causó la transición, no del estado alcanzado.

stateDiagram-v2
[*] --> cerrado
cerrado --> abierto : ABRIR_RATON
cerrado --> abierto : ABRIR_TECLADO / sonar
abierto --> cerrado : CERRAR
note right of abierto : entry enfocarPrimero

La equivalencia y su asimetría

Que ambos modelos tengan el mismo poder expresivo es un resultado clásico, y sus dos direcciones no cuestan lo mismo. Pasar de Moore a Mealy es inmediato: se define la salida de cada arista como la salida del estado al que apunta, es decir λ_mealy(q, a) = λ_moore(δ(q, a)). No hacen falta estados nuevos y la traducción es mecánica.

La dirección contraria es más cara. En una máquina de Mealy, un mismo estado puede alcanzarse por aristas que emiten salidas diferentes, y en Moore la salida tiene que ser única por estado. La solución es desdoblar: crear una copia del estado por cada salida distinta con la que se puede llegar a él, de modo que cada copia lleve su etiqueta. El resultado puede tener hasta tantos estados como el producto del número de estados por el de salidas posibles. Mealy es, por tanto, potencialmente más compacto, y esa compacidad es la razón de su popularidad en diseño de circuitos.

Hay una segunda asimetría, más sutil y más importante en la práctica. Una máquina de Moore emite la salida de su estado inicial antes de leer nada, de modo que ante una entrada de n símbolos produce n + 1 salidas. Una máquina de Mealy emite una salida por transición, de modo que produce exactamente n. Convertir entre ambas sin tener presente ese desajuste desplaza la secuencia de salida en una posición, y ese es el célebre retardo de un ciclo de las máquinas de Moore en electrónica digital.

Ese detalle tiene un eco directo en la interfaz que casi nadie relaciona con su origen. Cuando arrancas una máquina cuyo estado inicial lleva una acción entry, esa acción se ejecuta en el momento del arranque, antes de que llegue ningún evento: es exactamente la salida n + 1 de Moore. Si el efecto es lanzar una petición, la petición sale sola al montar; si es enfocar un campo, el foco se mueve sin que el usuario haya hecho nada. Ninguna de las dos cosas es un error —a menudo es justo lo que se quiere— pero ambas sorprenden a quien esperaba que los efectos solo ocurriesen como respuesta a algo. La sorpresa desaparece en cuanto uno recuerda que el estado inicial también es un estado, y que en Moore estar es suficiente.

Una tercera diferencia, esta puramente estructural, es que Moore obliga a que la salida sea función del destino y por tanto no puede depender de por dónde se llegó; Mealy puede depender del origen y del símbolo pero no puede expresar lo que ocurre mientras se permanece, porque sin transiciones no hay emisiones. Cada modelo es ciego a lo que el otro ve con naturalidad, y esa ceguera complementaria es la razón por la que un formalismo práctico termina ofreciendo ambos en lugar de elegir.

ℹ️
Por qué el hardware prefiere Moore y la lógica combinacional prefiere Mealy

En un circuito síncrono, la salida de una máquina de Moore depende solo de los biestables de estado, así que está registrada y es estable durante todo el ciclo de reloj: no tiene glitches y facilita cumplir los tiempos de propagación. La salida de una máquina de Mealy depende también de las entradas, así que puede cambiar en mitad de un ciclo y arrastrar cualquier ruido de la entrada, a cambio de reaccionar un ciclo antes. La disyuntiva es literalmente latencia contra estabilidad. La misma tensión reaparece en una interfaz, aunque no la llamemos así: un efecto en entry ocurre asociado a una situación estable y verificable, mientras que un efecto en la transición ocurre pegado al evento, antes de que el nuevo estado esté establecido.

El eco en los statecharts

Aquí es donde setenta años de teoría aterrizan en tu editor. Un motor de statecharts moderno no elige entre Mealy y Moore: ofrece los dos y deja la decisión en cada efecto. Las acciones entry y exit están ligadas al estado y son salida de Moore; las acciones declaradas sobre una transición concreta están ligadas a la arista y son salida de Mealy.

Lo que convierte esa dualidad en algo operativo es el orden de ejecución, que la especificación fija sin ambigüedad: al tomar una transición se ejecutan primero las acciones exit del estado de origen, después las acciones de la propia transición, y por último las acciones entry del estado de destino. Ese orden no es arbitrario, es la traducción operativa de la equivalencia: la parte Moore de salida, la parte Mealy en medio, la parte Moore de entrada.

const desplegable = {
  initial: "cerrado",
  states: {
    cerrado: {
      on: {
        ABRIR_RATON:   { target: "abierto" },
        ABRIR_TECLADO: { target: "abierto", actions: "sonar" },  // Mealy
      },
    },
    abierto: {
      entry: "enfocarPrimero",     // Moore: al entrar, vengas de donde vengas
      exit:  "restaurarFoco",      // Moore: al salir, vayas a donde vayas
      on: {
        CERRAR:    { target: "cerrado" },
        NAVEGAR:   { actions: "moverSeleccion" },              // interna: no reejecuta entry
        REABRIR:   { target: "abierto" },                       // externa: SI reejecuta exit y entry
      },
    },
  },
}

Las dos últimas líneas contienen el bug clásico de esta lección. Una transición interna maneja el evento sin salir del estado: no dispara exit ni entry, y es lo que se quiere cuando el evento solo actualiza algo dentro de la situación actual. Una transición externa hacia el propio estado sí lo abandona y vuelve a entrar, disparando exit y luego entry. La diferencia es invisible en un diagrama descuidado y visible al instante en producción, cuando cada pulsación de flecha vuelve a enfocar el primer elemento porque entry se está reejecutando en cada paso.

⚠️
El síntoma reconocible de haber elegido mal

Si un efecto se dispara de más, sospecha de una acción en entry con transiciones externas hacia el mismo estado, o de un entry en un estado al que se vuelve más veces de las que crees. Si un efecto se dispara de menos, sospecha de una acción colgada de una única transición cuando en realidad hay varias aristas que llegan al mismo destino y solo una la lleva. Ese segundo caso es el más caro porque el fallo no aparece hasta que alguien añade el segundo camino de entrada meses después, y entonces el efecto sencillamente no ocurre por esa ruta. Es la firma inconfundible de haber puesto en Mealy algo que era Moore: un efecto que debía ser propiedad del estado escrito como consecuencia de un evento.

El criterio: propiedad del estado o consecuencia del evento

La regla que resuelve casi todos los casos se enuncia como una pregunta. Si el efecto debe ocurrir siempre que el sistema esté en esa situación, sin importar cómo llegó, es una propiedad del estado y va en entry: lanzar la petición al entrar en cargando, enfocar el primer campo al abrir, arrancar el temporizador al entrar en esperando. Si el efecto depende de qué evento concreto provocó el cambio, es una consecuencia de la transición y va en la arista: registrar analítica con la causa, mostrar un mensaje distinto según si el cierre fue voluntario o por error, reproducir el sonido solo cuando se abrió con teclado.

Aplicada a un caso concreto, la regla se resuelve sin discusión. Una máquina de carga con reintento tiene efectos de los dos tipos y ninguno admite el otro sitio.

const carga = {
  initial: "inactivo",
  states: {
    inactivo: { on: { PEDIR: { target: "cargando" } } },
    cargando: {
      entry: "lanzarPeticion",       // Moore: vengas de inactivo o de fallo, se pide
      exit:  "abortarPeticion",      // Moore: salgas por exito, error o cancelacion
      on: {
        EXITO:    { target: "listo" },
        ERROR:    { target: "fallo", actions: "registrarCausa" },   // Mealy
        CANCELAR: { target: "inactivo", actions: "registrarCausa" },// Mealy
      },
    },
    fallo: { on: { PEDIR: { target: "cargando" } } },
    listo: {},
  },
}

Lanzar la petición está en entry porque debe ocurrir por los dos caminos que llegan a cargando, el inicial y el del reintento, y porque mañana puede haber un tercero. Abortarla está en exit porque debe ocurrir por las tres salidas y porque olvidarse de una sola de ellas produce una petición huérfana. Registrar la causa está en las aristas porque la causa es precisamente lo que distingue una arista de otra: es información del evento que el estado de destino no conserva.

Hay un segundo criterio, independiente del primero y a menudo decisivo: la gestión de recursos. Todo lo que se adquiere y hay que liberar —una suscripción, un temporizador, un bloqueo de scroll, un AbortController— pide entry y exit emparejados, porque esa pareja garantiza la liberación por todos los caminos de salida, incluidos los que aún no existen. Colgar la adquisición de una transición obliga a colgar la liberación de todas las transiciones salientes, y esa lista crece cada vez que alguien añade una arista. El primer olvido es una fuga, y llega siempre.

criterio va en entry / exit va en la transición
depende del evento de origen no
debe ocurrir por cualquier camino no
adquiere o libera un recurso sí, emparejado no
es idempotente y barato indiferente indiferente
debe reejecutarse al reentrar sí, con transición externa sí, siempre

Una última observación que ordena el conjunto: en la práctica lo Moore es lo que se documenta solo. Un lector que abre el diagrama ve el efecto escrito en la caja del estado y deduce el invariante sin más contexto; un efecto repartido por cinco aristas obliga a reconstruirlo mentalmente recorriéndolas todas. Por eso, ante duda genuina, entry es el sitio por defecto y la arista es la excepción que hay que justificar con una dependencia real del evento.

Elegir dónde vive la salida es decidir qué causa qué

Parece una decisión de ergonomía —dos sitios donde escribir lo mismo, elige el que te resulte cómodo— y es en realidad una afirmación sobre la causalidad de tu sistema. Poner un efecto en entry es declarar que ese efecto es consecuencia de estar en una situación, y por tanto que la situación es la causa suficiente: quien alcance ese estado por cualquier vía, incluida una que aún nadie ha escrito, obtendrá el efecto. Poner el efecto en una arista es declarar lo contrario, que la causa es el suceso concreto y que llegar al mismo sitio por otro camino no debe producirlo. Son dos ontologías distintas del mismo sistema, no dos estilos de escribir, y cuando el código elige una que contradice el dominio el resultado no es feo: es incorrecto, aunque tarde meses en manifestarse. Esta es también la razón por la que la distinción sobrevivió setenta años y tres generaciones de tecnología: no es un detalle de las máquinas finitas, es la pregunta general de si un efecto pertenece al ser o al devenir, y esa pregunta reaparece intacta en los ciclos de vida de los componentes, en los disparadores de una base de datos y en los reconciliadores que mantienen un sistema en su estado deseado. Toda arquitectura que separa lo declarativo de lo imperativo está eligiendo, sin nombrarlo, entre Moore y Mealy. La versión madura del criterio no es memorizar en qué campo va cada acción, sino adquirir el reflejo de preguntarse, ante cada efecto, si describe algo que es verdad mientras el sistema está de una manera, o algo que ocurre porque el sistema pasó de una manera a otra. Contestada esa pregunta, el campo donde escribirlo ya no es una decisión.

⚔️ Reparte la salida con criterio
  1. Modela un semáforo de tres luces como máquina de Moore y como máquina de Mealy. Compara el número de estados y decide cuál refleja mejor el dominio.
  2. Convierte una máquina de Mealy de tres estados a Moore desdoblando estados, y comprueba sobre una entrada concreta el desfase de una posición en la secuencia de salida.
  3. Toma una máquina real de tu aplicación y clasifica cada acción como Moore o Mealy. Anota las que están en el sitio equivocado según el criterio de la última sección.
  4. Busca en tu código un efecto adquirido en una transición cuya liberación esté repartida por varias aristas de salida. Reescríbelo como pareja entry y exit.
  5. Provoca a propósito el bug de la transición externa hacia el propio estado y observa el entry reejecutándose. Después conviértela en interna y confirma que desaparece.
  6. Añade una segunda arista que llegue a un estado cuyo efecto estaba colgado de la única transición existente, y comprueba que por el camino nuevo el efecto no ocurre. Corrígelo moviéndolo a entry.