Multi-type Messaging
In real world applications, actors will typically receive different message types and execute different behaviors based on the type received.
So far you've seen a simple example where an actor's message type is defined in the Actor::Msg associated type. More specifically, this defines an actor's mailbox type. To allow an actor to receive multiple message types, Picard provides a Receive<T> trait and the #[actor] attribute.
Let's see how these are used:
// Define the messages we'll use #[derive(Clone, Debug)] pub struct Add; #[derive(Clone, Debug)] pub struct Sub; #[derive(Clone, Debug)] pub struct Print; // Define the Actor and use the 'actor' attribute // to specify which messages it will receive #[actor(Add, Sub, Print)] struct Counter { count: u32, } impl Actor for Counter { // we used the #[actor] attribute so CounterMsg is the Msg type type Msg = CounterMsg; fn recv(&mut self, ctx: &Context<Self::Msg>, msg: Self::Msg, sender: Sender) { // Use the respective Receive<T> implementation self.receive(ctx, msg, sender); } } impl Receive<Add> for Counter { type Msg = CounterMsg; fn receive(&mut self, _ctx: &Context<Self::Msg>, _msg: Add, _sender: Sender) { self.count += 1; } } impl Receive<Sub> for Counter { type Msg = CounterMsg; fn receive(&mut self, _ctx: &Context<Self::Msg>, _msg: Sub, _sender: Sender) { self.count -= 1; } } impl Receive<Print> for Counter { type Msg = CounterMsg; fn receive(&mut self, _ctx: &Context<Self::Msg>, _msg: Print, _sender: Sender) { println!("Total counter value: {}", self.count); } } fn main() { let sys = ActorSystem::new().unwrap(); let props = Props::produce::<Counter>(); let actor = sys.actor_of_props(props, "counter").unwrap(); actor.tell(Add, None); actor.tell(Add, None); actor.tell(Sub, None); actor.tell(Print, None); // force main to wait before exiting program std::thread::sleep(Duration::from_millis(500)); }
In this example, we've used #actor[Add, Sub, Print] to set up the actor to receive Add, Sub and Print types. For each of these, the Receive<T> trait is implemented on the actor, defining how each message should be handled.
Note: When using the
#[actor()]attribute, the actor'sMsgassociated type should be set to '[DataType]Msg'. E.g. if an actor is a struct namedMyActor, then theActor::Msgassociated type will beMyActorMsg.
By utilizing Receive<T> and #[actor], complex message handling can be defined clearly and concisely. For more advanced messaging examples see Advanced Messaging.
Serializable messages
Enable the serde feature to require every actor message to implement serde::Serialize and serde::de::DeserializeOwned, for example to send messages to actors on another machine:
[dependencies]
picard = { version = "0.4", features = ["serde"] }
serde = { version = "1", features = ["derive"] }
#![allow(unused)] fn main() { #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Add; }
Creating an actor whose Msg type doesn't implement both traits is a compile error. The message enum generated by #[actor] derives them automatically, so only the types listed in #[actor(...)] need the derives. picard-cqrs's EntityMsg does the same.
A serializable message can't carry process-local values such as an std::sync::mpsc::Sender or an ActorRef. Give the actor those values when you create it, for example with actor_of_args, and send plain data in the message. When you need the sender's address, use the sender argument of recv.
Picard's own actors (channels, system actors) keep their local messages and are unaffected. Their messages use the LocalMessage bound, which is what the runtime requires internally. Message is LocalMessage plus the serde traits when the feature is enabled.
Cargo features are unified across a build, so enabling picard/serde in one crate applies it to every crate that uses Picard in the same build.
In the next section, we'll explore the relationship between actors and how actors form a hierarchy.