Skip to content

Functional State

The actor model requires each actor to designate the behavior for its next message. This is the mechanism for state transitions. In Casty, state is captured in closures: the message handler closes over the current state, and returning a new behavior with different closed-over values constitutes a state transition.

Consider a bank account actor that tracks a balance:

@dataclass(frozen=True)
class Deposit:
    amount: int

@dataclass(frozen=True)
class GetBalance:
    reply_to: ActorRef[int]

type AccountMsg = Deposit | GetBalance

def bank_account(balance: int = 0) -> Behavior[AccountMsg]:
    async def receive(ctx: ActorContext[AccountMsg], msg: AccountMsg) -> Behavior[AccountMsg]:
        match msg:
            case Deposit(amount):
                return bank_account(balance + amount)
            case GetBalance(reply_to):
                reply_to.tell(balance)
                return Behaviors.same()

    return Behaviors.receive(receive)

async def main() -> None:
    async with ActorSystem() as system:
        account = system.spawn(bank_account(), "account")
        account.tell(Deposit(100))
        account.tell(Deposit(50))
        await asyncio.sleep(0.1)

asyncio.run(main())

The line return bank_account(balance + amount) is the state transition. It creates a new ReceiveBehavior whose handler closes over balance + amount. There is no mutable field, no self.balance = ..., no nonlocal. The function call is the state transition.

This approach — called behavior recursion — makes state transitions explicit, traceable, and impossible to corrupt through accidental sharing. Each invocation of bank_account(n) produces a completely independent behavior value.


Next: Request-Reply