El actor model: el modelo de Hewitt
Antes de XState existió una idea. En 1973 Carl Hewitt propuso el actor como la unidad universal del cómputo concurrente: una entidad viva y aislada que no comparte memoria con nadie y solo interactúa enviando y recibiendo mensajes asíncronos. De ese único axioma —sin estado compartido, solo paso de mensajes— se derivan la ausencia de condiciones de carrera, la transparencia de ubicación y la tolerancia a fallos. Esta lección reconstruye el modelo desde sus tres capacidades fundacionales, lo contrasta con el objeto y el hilo, y explica por qué medio siglo después sigue siendo el cimiento de Erlang, Akka y de XState v5.
Mucho antes de que existiera un solo framework de estado, en 1973 Carl Hewitt formuló una pregunta radical: ¿y si la unidad mínima del cómputo no fuera el objeto ni el hilo, sino una entidad viva que no comparte absolutamente nada con las demás y solo se relaciona enviando mensajes? Esa entidad es el actor. El modelo que lleva su nombre no es una librería ni un patrón de diseño: es una teoría de la concurrencia, y es el suelo conceptual sobre el que XState v5 construye todo. Entenderlo aquí, en abstracto, hará que cada API del nivel te parezca inevitable en lugar de arbitraria.
- Enunciar las tres capacidades que, según Hewitt, definen a un actor.
- Contrastar el actor con el objeto y el hilo para ver qué prohíbe y qué gana.
- Entender el aislamiento total: sin memoria compartida, solo paso de mensajes.
- Reconocer el linaje del modelo: de Hewitt a Erlang, Akka y XState v5.
Un actor: tres capacidades
Hewitt redujo la concurrencia a una figura y tres verbos. La figura es el actor: una entidad con estado privado y un comportamiento. Los tres verbos describen todo lo que un actor puede hacer al recibir un mensaje, y absolutamente nada más está permitido. No hay una cuarta operación escondida, y esa parquedad es deliberada.
Enviar mensajes
Un actor puede mandar un número finito de mensajes a otros actores cuya dirección conoce. Enviar es la única forma de influir en otro: no existe la llamada a método ni el acceso a su memoria.
Crear actores
Un actor puede crear un número finito de actores nuevos. Así nace la jerarquía: el creador conoce la dirección del creado y puede delegarle trabajo o supervisarlo.
Designar comportamiento
Un actor decide cómo responderá al siguiente mensaje. Ese cambio de comportamiento ES la mutación de estado, pero encapsulada: nadie fuera del actor la observa jamás.
La fuerza del modelo está en lo que prohíbe. En el mundo de objetos e hilos, dos hilos pueden leer y escribir el mismo campo a la vez, y de ahí nacen las condiciones de carrera. Un actor no ofrece esa posibilidad: su estado es inaccesible desde fuera. La única manera de pedirle algo es depositar un mensaje en su buzón y esperar. La designación de comportamiento sustituye a la asignación compartida: en lugar de que otro escriba en tu memoria, tú decides qué harás con lo que llegue.
El tercer verbo es el más sutil y el que más nos costará reconocer luego en XState. Mutar el estado, en el modelo puro, no es asignar a una variable: es designar el comportamiento con el que se atenderá el próximo mensaje. Un actor no cambia un campo; se convierte en una versión de sí mismo que responderá distinto. Esa reformulación, que parece un rodeo, es lo que mantiene la mutación encerrada dentro de la frontera del actor.
// Designar comportamiento: el actor decide como respondera al SIGUIENTE mensaje.
// No muta memoria ajena; reemplaza su propia funcion de recepcion.
function cuentaAtras(restantes: number) {
return (msg: { tipo: "tic" }, self: Actor) => {
if (restantes === 0) return convertirse(self, terminado)
console.log(restantes)
return convertirse(self, cuentaAtras(restantes - 1)) // nuevo comportamiento
}
}
El actor frente al objeto y al hilo
Para situar el modelo conviene contrastarlo con las dos abstracciones que todo programador ya conoce. Un objeto encapsula estado, pero lo expone por métodos síncronos: quien tiene una referencia entra en su memoria y la lee o la muta en el acto, en el mismo hilo. Un hilo introduce ejecución concurrente, pero comparte memoria con los demás hilos, y de ahí nacen los candados. El actor toma lo bueno de ambos y descarta lo peligroso: encapsula estado como el objeto, corre concurrentemente como el hilo, pero no expone su memoria ni la comparte con nadie.
| Aspecto | Objeto | Hilo | Actor |
|---|---|---|---|
| Estado | Encapsulado, accesible por métodos | Compartido con otros hilos | Privado, inaccesible desde fuera |
| Comunicación | Llamada síncrona a método | Memoria compartida y candados | Mensaje asíncrono al buzón |
| Concurrencia | No, por sí solo | Sí, con riesgo de carreras | Sí, sin carreras por construcción |
| Fallo | Excepción que propaga | Puede corromper memoria común | Contenido en el actor y su subárbol |
La fila de la comunicación lo decide todo. La llamada a método acopla en el tiempo —el que llama espera— y en el espacio —ambos comparten hilo—. El mensaje asíncrono rompe los dos acoplamientos: el emisor no espera y no necesita estar en el mismo lugar. Esa doble ruptura es lo que permite repartir un programa de actores entre núcleos o entre máquinas sin cambiar su lógica, algo que ni objetos ni hilos consienten sin reescribirse de raíz.
Aislamiento y el buzón: por qué no hay carreras
Cada actor posee un buzón privado —una cola— donde sus mensajes se acumulan. El actor los procesa de uno en uno, secuencialmente. Esta serialización es la clave: como solo se atiende un mensaje cada vez y el estado nunca se comparte, dos operaciones jamás pisan el mismo dato al mismo tiempo. La condición de carrera no se evita con disciplina ni con candados; sencillamente no puede expresarse.
// Un actor en pseudocodigo: estado privado y un buzon que procesa de a uno.
type Mensaje =
| { tipo: "incrementar" }
| { tipo: "leer"; responder: Actor }
function contador() {
let valor = 0 // estado privado, invisible desde fuera
return (msg: Mensaje) => { // se procesa un mensaje cada vez
if (msg.tipo === "incrementar") valor += 1
if (msg.tipo === "leer") enviar(msg.responder, { valor })
}
}
Fíjate en el mensaje leer: no devuelve el valor con un return, sino que incluye la dirección de otro actor al que responder. En el modelo puro no existe la lectura síncrona; incluso preguntar es enviar un mensaje y esperar la respuesta en tu propio buzón. Esa asimetría, que al principio incomoda, es lo que permite que el actor que pregunta no se quede bloqueado esperando a otro.
flowchart LR M1[mensaje 1] --> Q[buzon] M2[mensaje 2] --> Q M3[mensaje 3] --> Q Q -- de uno en uno --> AC[actor procesa] style Q fill:#f9e2af,color:#11111b style AC fill:#cba6f7,color:#11111b
Un runtime de actores no es más que un bucle que saca mensajes del buzón y aplica el comportamiento actual a cada uno. En pseudocódigo cabe en unas líneas, y deja ver que no hay magia: solo una cola, un estado privado y una función que se aplica de a un mensaje.
// Un micro-runtime: crear un actor, encolar mensajes y procesarlos en orden.
function crear(comportamiento: Comportamiento) {
const buzon: Mensaje[] = []
const self = {
enviar(msg: Mensaje) { // depositar en el buzon, nunca ejecutar ya
buzon.push(msg)
planificar()
},
}
function planificar() {
queueMicrotask(() => {
while (buzon.length) comportamiento(buzon.shift()!, self) // de a uno
})
}
return self
}
En el modelo de hilos y candados, la corrección depende de que el programador recuerde adquirir el candado correcto antes de tocar cada dato compartido; olvidarlo una sola vez introduce un bug no determinista. El buzón hace ese trabajo de forma estructural: como el estado solo se toca al procesar un mensaje, y los mensajes se procesan de uno en uno, el acceso exclusivo está garantizado sin que nadie tenga que acordarse de nada. La exclusión mutua deja de ser una convención y pasa a ser la forma del sistema.
Transparencia de ubicación y supervisión
Un actor no conoce a otro por un puntero a su memoria, sino por una dirección: un identificador opaco al que enviar mensajes. Esa dirección no revela si el actor destino vive en el mismo hilo, en otro núcleo o en otra máquina al otro lado de la red. A esa propiedad se le llama transparencia de ubicación, y es lo que permite que el mismo código escale de un hilo a un clúster: enviar es siempre asíncrono, tanto si el destino está al lado como si está lejos.
flowchart LR A[actor A] -- mensaje --> B[actor B] A -- crea --> C[actor C] B -. respuesta asincrona .-> A C -- mensaje --> B style A fill:#89b4fa,color:#11111b style B fill:#cba6f7,color:#11111b style C fill:#a6e3a1,color:#11111b
De la capacidad de crear actores surge la supervisión. Como el creador conoce la dirección de sus hijos, puede vigilarlos: si uno falla, el padre recibe la noticia y decide reiniciarlo, ignorarlo o propagar el fallo hacia arriba. Erlang convirtió esta idea en su lema —let it crash, déjalo fallar— y construyó sistemas de telefonía con años de disponibilidad sobre ella. El fallo de un actor queda contenido: no corrompe la memoria de sus hermanos porque no hay memoria común que corromper.
El modelo de Hewitt no se quedó en la teoría. Erlang (1986) lo llevó a producción con procesos ligerísimos y árboles de supervisión, y hoy sostiene WhatsApp y buena parte de la infraestructura de telecomunicaciones. Akka lo trajo a la JVM. XState v5 lo trae a JavaScript y TypeScript: en 2026 su unidad central no es la máquina de estados, sino el actor. Los nombres cambian —proceso, actor, servicio—, pero los tres axiomas de 1973 se reconocen intactos en cada uno.
La lectura ingenua del modelo de actores es que es un objeto al que le han quitado cosas: no puedes leer su estado, no puedes llamar a sus métodos, no puedes compartir memoria con él. La lectura profunda es la contraria. Cada una de esas prohibiciones es exactamente lo que compra una garantía. No compartir memoria compra la ausencia de condiciones de carrera. Comunicar solo por mensajes compra la transparencia de ubicación, y con ella la posibilidad de distribuir el sistema sin reescribirlo. Procesar de uno en uno compra la exclusión mutua sin candados. Designar comportamiento en vez de mutar memoria ajena compra que el fallo de un actor quede contenido en su propia frontera. El actor no es un objeto empobrecido: es un objeto al que se le ha dado el único límite que lo hace componible a escala arbitraria. Cuando en las próximas lecciones veas spawn, sendTo y system, no estarás aprendiendo una API nueva, sino reconociendo estos tres verbos vestidos de TypeScript. XState no inventó nada aquí; encarnó una idea de medio siglo, y lo hizo porque esa idea sigue siendo la forma más robusta que conocemos de domar la complejidad de un estado que vive, cambia y falla.
- Enuncia con tus palabras los tres verbos de Hewitt y da un ejemplo de cada uno en una app real (un formulario, un reproductor, un carrito).
- Reescribe el
contadoren pseudocódigo para que acepte un mensajerestary otroreiniciar, sin exponer nuncavalordirectamente. - Convierte el
contadoral estilo de designación de comportamiento: en vez de mutarvalor, que cada mensaje devuelva un nuevo comportamiento con el valor actualizado. - Explica por qué el mensaje
leernecesita llevar una dirección de respuesta en lugar de devolver un valor conreturn. - Rellena la fila de una tabla propia comparando objeto, hilo y actor en una quinta dimensión: la facilidad para hacer pruebas en aislamiento.
- Argumenta por qué el aislamiento total, lejos de ser una limitación, es lo que permite distribuir el sistema en varias máquinas sin cambiar el código.