An Erlang/Elixir inspired actor library for Rust
- stable/nightly Rust
Define your actor
pub struct MyActor;
Implement the GenServer trait
impl GenServer for MyActor {
type T = MyMessage;
type S = ();
type E = MyError;
fn init(&self, _tx: &ActorSender<Self::T>, state: &mut Self::S) -> InitResult<Self::E> {
// initialization implementation
Ok(None)
}
// Implement overrides for default implementations for additional callbacks
}
Start your actor
use wonder::actor;
fn main() {
let actor = actor::Builder::new(MyActor).start(()).unwrap();
}
To create a GenServer actor you need to create a struct (perhaps a unit-like struct?) and implement the GenServer trait.
struct MyActor;
To implement the GenServer trait you need to define 3 associated types
T
- A public enum type for messages to be sent to and from the GenServer.S
- The state which the actor will own and maintain.()
can be provided in the case of an actor who will manage no state.E
- A public enum type for errors to be returned by your GenServer.
struct MyState {
pub initialized: bool,
}
#[derive(Debug)]
enum MyError {
DirtyState,
}
#[derive(Debug)]
enum MyMessage {
State(bool),
GetState,
SetState(bool),
}
impl GenServer for MyActor {
type T = MyMessage;
type S = MyState;
// type S = (); // no state
type E = MyError;
// ... callbacks
}
note: It is required for both the custom error and message enums to derive the Debug trait.
The GenServer trait exposes 4 callbacks; one of which is required while the remaining three have default implementations making them optional to implement.
analogous to GenServer:init/1
Handles initialization of the newly created actor. As with proccess initialization in Elixir/Erlang, this is a blocking call. This function will be called once while the actor is starting.
The init function must returns an InitResult
which is either Ok(Option<u64>)
or Err(E)
where E is your custom error type. The optional u64
is the timeout value (in milliseconds) for the actor.
impl GenServer for MyActor {
type T = MyMessage;
type S = MyState;
type E = MyError;
fn init(&self, atx: &ActorSender<Self::T>, state: &mut Self::S) -> InitResult<Self::E> {
// perform some initialization
Ok(None)
}
}
Parameters
atx
- Channel sender for the running actor. This is equivalent to sending to "self" in Elixir/Erlang.state
- A mutable reference to the state that the running actor owns.
note:
InitResult
is analogous to the return tuple for theinit/1
callback in Elixir; ie.{:ok, state}
or{:stop, reason}
analogous to GenServer:handle_call/3
Handles synchronous messages sent to the running actor.
impl GenServer for MyActor {
type T = MyMessage;
type S = MyState;
type E = MyError;
// ... other callbacks ...
fn handle_call(&self, msg: Self::T, tx: &ActorSender<Self::T>,
atx: &ActorSender<Self::T>, state: &mut Self::S) -> HandleResult<Self::T> {
match msg {
MyMessage::GetState => HandleResult::Reply(MyMessage::State(state.initialized), None),
MyMessage::SetState(value) => {
state.initialized = value;
HandleResult::Reply(MyMessage::Ok, None)
}
_ => HandleResult::Stop(StopReason::Fatal(String::from("unhandled call")), None),
}
}
}
Parameters
tx
- Channel sender for the caller who started the actor. This is equivalent to sending to the caller's PID in Elixir/Erlang.atx
- Same asinit/3
.state
- Same asinit/3
.
note:
HandleResult
is analogous to the return tuple for thehandle_call/3
callback in Elixir/Erlang.
analogous to GenServer:handle_cast/2
Handles asynchronous messages sent to the running actor.
impl GenServer for MyActor {
type T = MyMessage;
type S = MyState;
type E = MyError;
// ... other callbacks ...
fn handle_cast(&self, msg: Self::T, atx: &ActorSender<Self::T>, state: &mut Self::S) -> HandleResult<Self::T> {
match msg {
MyMessage::SetState(value) => {
state.initialized = value;
HandleResult::NoReply(None)
},
_ => HandleResult::NoReply(None)
}
}
}
Parameters
atx
- Same asinit/3
state
- Same asinit/3
note:
HandleResult
is analogous to the return tuple for thehandle_cast/2
callback in Elixir/Erlang.
analogous to matching
handle_info(:timeout, _state)
in Elixir
Called when a timeout is set and the required amount of time has elapsed. A timeout can be set by including a timeout value in a HandleResult
or InitResult
returned by one of the GenServer callbacks.
The timeout can be used for various reason, but a great example is a pattern to perform late initialization. If your actor has a long running initialization period you can timeout immediately and perform initialization within the handle_timeout
callback.
impl GenServer for MyActor {
type T = MyMessage;
type S = MyState;
type E = MyError;
fn init(&self, atx: &ActorSender<Self::T>, state: &mut Self::S) -> InitResult<Self::E> {
Ok(Some(0))
}
fn handle_timeout(&self, atx: &ActorSender<Self::T>, state: &mut Self::S) -> HandleResult<Self::T> {
// long running function for late initialization
HandleResult::NoReply(None)
}
}
Parameters
atx
- Same asinit/3
state
- Same asinit/3
note:
HandleResult
is analogous to the return tuple for thehandle_info/2
callback in Elixir/Erlang.
Jamie Winsor (jamie@vialstudios.com)