🏠

actor patterns

state machines, fan-out, chatter, and more.

pattern 1 β€” state machine (rules= shines)

Classic traffic light. Different message types trigger different transitions:

actor = FX.StartActor(
    input=topic,
    initial_state=ET.Green(),
    rules={
        ('tick',      ET.Green):  constant_func([[FX.Print(content='πŸŸ’β†’πŸŸ‘')], ET.Yellow()]),
        ('tick',      ET.Yellow): constant_func([[FX.Print(content='πŸŸ‘β†’πŸ”΄')], ET.Red()]),
        ('tick',      ET.Red):    constant_func([[FX.Print(content='πŸ”΄β†’πŸŸ’')], ET.Green()]),
        ('emergency', Any):       constant_func([[FX.Print(content='πŸš¨β†’πŸ”΄')], ET.Red()]),
    },
) | run

Rules are tuples of (msg_pattern, state_pattern). First match wins. Put narrow rules first, catch-alls last.

pattern-matching cheatsheet

patternmatches when…
Anyanything
Intvalue is an Int
ET.Dogvalue is an ET.Dog entity
greater_than(5)op returns True for value
'tick'equality
{1, 2, 3}value is in set

pattern 2 β€” accumulator (handler= shines)

Running stats, counters, log aggregation:

@zef_function
def running_sum(msg: Int, state: Int) -> Array:
    total = state + msg
    return [[FX.Print(content=f'total={total}')], total]

actor = FX.StartActor(input=topic, initial_state=0, handler=running_sum) | run

pattern 3 β€” fan-out (one topic, many actors)

Multiple actors subscribe to the same topic. Each processes every message independently.

topic = ET.Topic('πŸƒ-c3c3c3c3c3c3c3c3c3c3')

a1 = FX.StartActor(input=topic, initial_state=0, handler=doubler) | run
a2 = FX.StartActor(input=topic, initial_state=0, handler=squarer) | run

FX.Publish(target=topic, content=5) | run
# doubler prints 10
# squarer prints 25
fan-out as a diagram
β”Œβ”€β”€β”€β–Ά actor A (doubler) topic ────┼───▢ actor B (squarer) └───▢ actor C (logger) One publish, three independent executions.

pattern 4 β€” actor-to-actor chatter

An actor can publish to another topic as part of its effects list:

topic_a = ET.Topic('πŸƒ-a1a1a1a1a1a1a1a1a1a1')
topic_b = ET.Topic('πŸƒ-b2b2b2b2b2b2b2b2b2b2')

player_a = FX.StartActor(
    input=topic_a,
    initial_state=ET.Ready(),
    rules={
        (Any, Any): constant_func([
            [FX.Print(content='πŸ“ A'),
             FX.Publish(target=topic_b, content='ball')],
            ET.Ready(),
        ]),
    },
) | run

player_b = FX.StartActor(
    input=topic_b,
    initial_state=ET.Ready(),
    rules={
        (Any, Any): constant_func([
            [FX.Print(content='πŸ“ B'),
             FX.Publish(target=topic_a, content='ball')],
            ET.Ready(),
        ]),
    },
) | run

# serve
FX.Publish(target=topic_a, content='ball') | run
# πŸ“ A
# πŸ“ B
# πŸ“ A
# πŸ“ B
# ... bouncing at ~100k msg/s in Rust

pattern 5 β€” coordinator β†’ subsystems

A coordinator receives high-level commands and fans out to subordinate topics:

coordinator = FX.StartActor(
    input=t_control,
    initial_state=ET.Normal(),
    rules={
        ('fire', ET.Normal): constant_func([
            [FX.Publish(target=t_alarm,     content='sound'),
             FX.Publish(target=t_sprinkler, content='activate'),
             FX.Publish(target=t_doors,     content='unlock')],
            ET.Emergency(),
        ]),
        ('all_clear', ET.Emergency): constant_func([
            [FX.Publish(target=t_alarm,     content='silence'),
             FX.Publish(target=t_sprinkler, content='deactivate'),
             FX.Publish(target=t_doors,     content='lock')],
            ET.Normal(),
        ]),
    },
) | run

Each subordinate (alarm, sprinkler, doors) is its own actor on its own topic. Coordinator just fires messages.

pattern 6 β€” per-connection actor (for websockets)

When someone connects to your WebSocket, spawn a fresh actor just for them:

FX.StartHTTPServer(
    routes={
        '/ws': ET.WebSocket(
            actor=FX.StartActor(
                initial_state=ET.Guest(),
                rules={
                    (ET.WSClientConnected, ET.Guest):       on_connect,
                    (String,                ET.Connected):   on_message,
                    (ET.WSClientDisconnected, Any):         on_disconnect,
                },
            ),
        ),
    },
    port=8000,
) | run

Each client = one actor. Private state per connection. The actor dies when the client disconnects.

More on WebSocket actors in chapter 22.

fizzbuzz as an actor

@zef_function
def fizzbuzz(n: Int, state: Int) -> Array:
    if n % 15 == 0: out = 'FizzBuzz!'
    elif n % 3 == 0: out = 'Fizz'
    elif n % 5 == 0: out = 'Buzz'
    else: out = str(n)
    return [[FX.Print(content=out)], state + 1]

actor = FX.StartActor(input=topic, initial_state=0, handler=fizzbuzz) | run
for i in range(1, 16):
    FX.Publish(target=topic, content=i) | run

common shape tips

state shape is your choice

The state can be any zef value. Use the shape that fits your model:

introspect while running

# What's the current state of the traffic light?
FX.QueryActorState(actor=traffic_light) | run

# All logins, with just the username field extracted
FX.QueryActorState(transform=F.username) | run

# How many items does each actor have in its state?
FX.QueryActorState(transform=length) | run

build a door state-machine

States: ET.Locked(), ET.Unlocked(), ET.Open(). Messages: 'unlock', 'lock', 'open', 'close'. Rules:

solution
actor = FX.StartActor(
    input=topic,
    initial_state=ET.Locked(),
    rules={
        ('unlock', ET.Locked):   constant_func([[FX.Print(content='πŸ”“')], ET.Unlocked()]),
        ('lock',   ET.Unlocked): constant_func([[FX.Print(content='πŸ”’')], ET.Locked()]),
        ('open',   ET.Unlocked): constant_func([[FX.Print(content='πŸšͺ')], ET.Open()]),
        ('close',  ET.Open):     constant_func([[FX.Print(content='β–₯')],  ET.Unlocked()]),
        (Any, Any):                    constant_func([[FX.Print(content='illegal')], Z]),   # keep state
    },
) | run

Next up: topics, SubscribeFX and timers. β†’