A device actor
Keep state in an actor, tell it readings and ask it for the last one.
A device records temperature readings and answers when asked for the last one. In this step you write it as an actor with state, and you talk to it in both ways tellus offers: tell and ask.
The messages
A device understands two messages:
enum DeviceMessage {
Record(f64),
Read(ReplyTo<Option<f64>>),
}Record carries a reading. Read asks for the last reading and carries a ReplyTo, the destination for the answer. A ReplyTo is used exactly once: calling reply consumes it.
The actor
struct Device;
impl Actor for Device {
type Message = DeviceMessage;
type State = Option<f64>;
type Error = Infallible;
fn init(&self, _: &ActorContext<Self::Message>) -> Result<Self::State, Self::Error> {
Ok(None)
}
fn receive(
&self,
_: &ActorContext<Self::Message>,
incoming: Incoming<Self::Message>,
last_reading: Self::State,
) -> Result<Control<Self::State>, Self::Error> {
let Incoming::Message(message) = incoming else {
unreachable!("the device watches no actor, hence never gets a terminated signal")
};
match message {
DeviceMessage::Record(degrees) => Ok(Control::Continue(Some(degrees))),
DeviceMessage::Read(reply_to) => {
reply_to.reply(last_reading);
Ok(Control::Continue(last_reading))
}
}
}
}The state is Option<f64>: no reading at first, later the last one. receive gets the current state by value and returns the state for the next message. For Record that is the new reading. For Read the device replies and keeps its state unchanged.
There is no &mut self and no field to update. The state lives in the return value, so you can read every transition in one place, the match.
The let ... else at the top deals with Incoming. Besides messages an actor can receive signals about other actors terminating. The device does not watch any actor yet, so it never gets one; you will meet signals in step 4.
Talking to the device
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let system = ActorSystem::new(Device);
let device = system.root();
device.tell(DeviceMessage::Record(21.5));
device.tell(DeviceMessage::Record(22.0));
let last_reading = device
.ask(Duration::from_secs(1), DeviceMessage::Read)
.await
.context("asking for the last reading")?;
match last_reading {
Some(degrees) => println!("Last reading: {degrees:.1}"),
None => println!("No reading yet"),
}
system.stop();
system
.terminated()
.await
.context("awaiting actor system termination")
}tell sends a message and returns at once. It neither waits for the message to be handled nor reports whether it arrived. That makes it fast, and it is how actors talk to each other.
ask is for code outside of actors, like main here. It builds the request around a ReplyTo, sends it and waits for the reply, at most for the given duration. Passing DeviceMessage::Read works because the variant is a function from ReplyTo to a message.
The mailbox delivers messages in the order they were sent. So the Read is handled after both readings, and the answer is always the second one. At the end, system.stop() stops the root actor and terminated waits for it.
Run it
From a clone of the tellus repository:
cargo run --quiet -p tellus --example getting_started_02_deviceIt prints:
Last reading: 22.0What you learned
An actor's state is a value that receive takes and returns. tell sends without waiting, ask waits for a reply, and messages from one sender arrive in order. Next, you let one actor spawn others.