The actor model
๐ฌ behind the scenes
THAT Conference Texas 2024
David Khourshid ยท @davidkpiano
stately.ai
Setting the scene:
Dealing with complex logic
Flashback:
Origin story
A Universal Modular ACTOR Formalism for Artificial Intelligence (1973)
Carl Hewitt, Peter Bishop, Richard Steiger
Alan Kay
- Coined "OOP"
- Emphasized message-passing
- Basically the Actor Model
Gul Agha
- Formalized the actor model
Actors: A Model of Concurrent Computation in Distributed Systems
Alan Kay
Easier than
you may think
State machines ๐ตโ๐ซ
๐ฉโ๐ป
๐ง
๐ฉโ๐ณ
โ๏ธโ
๐
๐ตโ
๐ต
๐โ
โ๏ธ
โ๏ธ
Acting:
The real world
๐ฉโ๐ป
๐ง
I would like a coffee...
What would you like?
ย Actorย
I would like a coffee, please.
ย Actorย
๐ญ
๐ฌ
๐ฌ
Here you go. โ๏ธ
Thanks!
What is an actor?
๐ค All computation is performed within an actor
๐ฉ Actors can communicate only through messages
๐ฅ In response to a message, an actor can:
๐ฌ Send messages to other actors
๐จโ๐งโ๐ฆ Create a finite number of child actors
๐ (๐ฅ)
Behavior
State
๐ญ Change its state/behavior
A
B
โ๏ธ
โ๏ธ
C
โ๏ธ
๐ค All computation is performed within an actor
๐ฉ Actors can communicate only through messages
๐ฅ In response to a message, an actor can:
๐ฌ Send messages to other actors
๐จโ๐งโ๐ฆ Create a finite number of child actors
๐ญ Change its state/behavior
What is an actor?
A
โ๏ธ
๐ค All computation is performed within an actor
๐ฉ Actors can communicate only through messages
๐ฅ In response to a message, an actor can:
๐ฌ Send messages to other actors
๐จโ๐งโ๐ฆ Create a finite number of child actors
๐ญ Change its state/behavior
A โ A'
What is an actor?
A
โ๏ธ
๐ค All computation is performed within an actor
๐ฉ Actors can communicate only through messages
๐ฅ In response to a message, an actor can:
๐ฌ Send messages to other actors
๐จโ๐งโ๐ฆ Create a finite number of child actors
๐ญ Change its state/behavior
B
โ๏ธ
What is an actor?
A
โ๏ธ
๐ค All computation is performed within an actor
๐ฉ Actors can communicate only through messages
๐ฅ In response to a message, an actor can:
๐ฌ Send messages to other actors
๐จโ๐งโ๐ฆ Create a finite number of child actors
๐ญ Change its state/behavior
๐
๐
What is an actor?
An actor's script
- ๐ฌ Send & receive messages
- ๐ญ Change its internal state
- ๐จโ๐ฉโ๐งโ๐ฆ Spawn child actors
{
orders: [/* ... */],
inventory: {
// ...
},
sales: 134.65
}
An actor's script
Actor System
โ๏ธ
state 1
โ๏ธ
state 2
โ๏ธ
โ๏ธ
processing...
state ??
Right on queue:
Actor mailboxes
โ๏ธ
โ๏ธ
โ๏ธ
โ๏ธ
Right on queue:
Actor mailboxes
โ๏ธ
state 1
โ๏ธ
โ๏ธ
โ๏ธ
Right on queue:
Actor mailboxes
โ๏ธ
state 1
โ๏ธ
state 2
โ๏ธ
โ๏ธ
Right on queue:
Actor mailboxes
โ๏ธ
state 1
โ๏ธ
state 2
โ๏ธ
โ๏ธ
state 3
โ๏ธ
Right on queue:
Actor mailboxes
โ๏ธ
state 1
โ๏ธ
state 2
โ๏ธ
โ๏ธ
state 3
โ๏ธ
state 4
Right on queue:
Actor mailboxes
Actors backstage
Backend applications
Actors onstage
Frontend applications
Rehearsal time:ย
Making an actor
function createActor(idk) {
// ...
return {
send: (event) => {
// ...
}
};
}
const actor = createActor();
actor.send({ type: 'inc' });
function createActor(idk) {
let state = {
count: 0
};
return {
send: (event) => {
// ...
if (event.type === 'increment') {
state.count++;
}
}
};
}
const actor = createActor();
actor.send({ type: 'inc' });
function createActor(transition) {
let state = {
count: 0
};
return {
send: (event) => {
state = transition(state, event);
}
};
}
const actor = createActor(/* ... */);
actor.send({ type: 'inc' });
function createActor(logic) {
let state = logic.initialState;
return {
send: (event) => {
state = logic.transition(state, event);
}
};
}
const actor = createActor({
logic: (state, event) => {/* ... */},
initialState: {/* ... */}
});
actor.send({ type: 'inc' });
Don't call us,
we'll call you:
Subscribing to actors
function createActor(logic) {
let state = logic.initialState;
const observers = new Set();
return {
send: (event) => {
state = logic.transition(state, event);
observers.forEach((observer) => {
observer.next(state);
});
},
subscribe: (observer) => {
observers.add(observer);
return () => {
observers.delete(observer);
};
}
};
}
const actor = createActor({
transition: (state, event) => {
if (event.type === 'inc') {
return { ...state, count: state.count + 1 };
}
return state;
},
initialState: { count: 0 }
});
actor.subscribe((s) => {
console.log(s);
});
actor.send({ type: 'inc' });
// => { count: 1 }
function createActor(logic) {
let state = {
status: 'inactive',
context: logic.initialContext
};
const observers = new Set();
return {
send: (event) => {
state = logic.transition(state.context, event);
observers.forEach((observer) => {
observer.next(state);
});
},
subscribe: (observer) => {
observers.add(observer);
return () => {
observers.delete(observer);
};
},
start: () => {
state.status = 'active';
observers.forEach((observer) => {
observer.next(state);
});
}
};
}
const actor = createActor({
transition: (ctx, event) => {
if (event.type === 'inc') {
return { ...ctx, count: ctx.count + 1 };
}
return ctx;
},
initialContext: { count: 0 }
});
actor.subscribe((s) => {
console.log(s);
});
actor.start();
// { context: { count: 0 } }
actor.send({ type: 'inc' });
// { context: { count: 1 } }
Special FX:
Managing side-effects
function createActor(logic) {
let state = {
status: 'inactive',
context: logic.initialContext
};
const observers = new Set();
const actor = {
send: (event) => {
state = logic.transition(
state.context,
event,
actor
);
observers.forEach((observer) => {
observer.next(state);
});
},
subscribe: (observer) => {
observers.add(observer);
return () => {
observers.delete(observer);
};
},
start: () => {
state.status = 'active';
observers.forEach((observer) => {
observer.next(state);
});
}
};
return actor;
}
const actor = createActor({
transition: (ctx, event, self) => {
if (event.type === 'load') {
const promise = new Promise((res) => {
setTimeout(() => {
res({ name: 'David' });
}, 1000);
});
promise.then((output) => {
self.send({ type: 'resolve', data: output });
});
return ctx;
} else if (event.type === 'resolve') {
return {
...ctx,
user: event.output
};
}
return ctx;
},
initialContext: { user: null }
});
actor.subscribe((s) => {
console.log(s);
});
actor.start();
// { context: { count: 0 } }
actor.send({ type: 'inc' });
// { context: { count: 1 } }
Guest appearance:
Actors in XState
npm i xstate
stately.ai/docs
import { createActor } from 'xstate';
import { someLogic } from './someLogic';
const actor = createActor(someLogic);
actor.subscribe((snapshot) => {
console.log(snapshot);
});
actor.start();
import { fromTransition, createActor } from 'xstate';
const counterLogic = fromTransition(
// Behavior
(state, event) => {
if (event.type === 'inc') {
return {
...state,
count: state.count + 1
};
}
return state;
},
// Initial state
{ count: 0 }
);
const counterActor = createActor(counterLogic);
counterActor.subscribe(/* ... */);
counterActor.start();
counterActor.send({ type: 'inc' });
import { fromTransition, createActor } from 'xstate';
const counterLogic = fromTransition(
// Behavior
(state, event) => {
if (event.type === 'inc') {
return {
...state,
count: state.count + 1
};
}
return state;
},
// Initial state
// with input
({ input }) => ({
count: input.initialCount
})
);
const counterActor = createActor(counterLogic, {
input: {
initialCount: 100
}
});
counterActor.subscribe(/* ... */);
counterActor.start();
counterActor.send({ type: 'inc' });
import { fromPromise, createActor } from 'xstate';
import { fetchUser } from './fetchUser';
const promiseLogic = fromPromise(async ({ input }) => {
const user = await fetchUser(input.userId);
return user;
});
const promiseActor = createActor(promiseLogic, {
input: { userId: 'user42' }
});
promiseActor.subscribe((s) => {
if (s.status === 'done') {
console.log(s.output);
}
});
promiseActor.start();
import { setup, createActor } from 'xstate';
const counterMachine = setup({
actors: {
promiseLogic,
counterLogic
}
}).createMachine({
initial: 'gettingUser',
states: {
gettingUser: {
invoke: {
src: 'promiseLogic',
input: {/* ... */},
onDone: {
target: 'counting'
}
}
},
counting: {
invoke: {
id: 'counter',
src: 'counterLogic',
input: {
initialCount: 0
},
onSnapshot: {
target: 'reachedMaxCount',
guard: ({ event }) => {
return event.snapshot.context.count === 10;
}
}
},
on: {
inc: {
actions: sendTo('counter', { type: 'inc' })
}
}
},
reachedMaxCount: {
type: 'final'
}
}
});
const counterActor = createActor(counterMachine);
counterActor.subscribe((s) => {
console.log(s);
});
counterActor.start();
counterActor.send({ type: 'inc' });
Visual FX:
Visualizing actors
Stately Inspect: Demo
npm i @statelyai/inspect
import { createBrowserInspector } from '@statelyai/inspect';
const inspector = createBrowserInspector();
inspector.actor('speaker');
inspector.actor('listener');
inspector.event('speaker', 'question?', {
source: 'listener'
});
inspector.event('listener', 'answer!', {
source: 'speaker'
});
Streaming live:
Location transparency
Understudies:
Fault tolerance
Actor critics:
Pros and cons
- Easy to scale
- Fault tolerance
- Location transparency
- No shared state
- Event-driven
- "Microservice hell"
- Learning curve
- Indirection
- Unfamiliarity
THAT Conference 2024
David Khourshid ยท @davidkpiano
stately.ai
End scene:
Final credits
THAT Conference 2024
David Khourshid ยท @davidkpiano
stately.ai
Improv time:
Any questions?
The actor model, behind the scenes
By David Khourshid
The actor model, behind the scenes
- 433