Expand description
An intuitive actor model for Rust with minimal boilerplate, no manual messages and typed arguments.
Note that this crate is under construction. Although used in production, work is done on making an intuitive API, documententation and remaining features. Pre 1.0 the API may break at any time!
Actify is an actor model built on Tokio that allows annotating any regular implementation block of your own type with the actify! macro. By generating the boilerplate code for you, a few key benefits are provided:
- Async actor model build on Tokio and channels
- Access to actors through clonable handles
- Types arguments on the methods from your actor, exposed through the handle
- No need to define message structs or enums!
§Main functionality of actify!
Consider the following example, in which you want to turn your custom Greeter into an actor:
#[actify]
impl Greeter {
fn say_hi(&self, name: String) -> String {
format!("hi {}", name)
}
}
#[tokio::main]
async fn main() {
// An actify handle is created and initialized with the Greeter struct
let handle = Handle::new(Greeter {});
// The say_hi method is made available on its handle through the actify! macro
let greeting = handle.say_hi("Alfred".to_string()).await.unwrap();
// The method is executed on the initialized Greeter and returned through the handle
assert_eq!(greeting, "hi Alfred".to_string())
}
This roughly desugars to:
impl Greeter {
fn say_hi(&self, name: String) -> String {
format!("hi {}", name)
}
}
// Defines the custom function signatures that should be added to the handle
#[async_trait::async_trait]
pub trait GreeterHandle {
async fn say_hi(&self, name: String) -> Result<String, ActorError>;
}
// Implements the methods on the handle, and calls the generated method for the actor
#[async_trait::async_trait]
impl GreeterHandle for Handle<Greeter> {
async fn say_hi(&self, name: String) -> Result<String, ActorError> {
let res = self
.send_job(FnType::Inner(Box::new(GreeterActor::_say_hi)), Box::new(name))
.await?;
Ok(*res.downcast().unwrap())
}
}
// Defines the wrappers that execute the original methods on the struct in the actor
trait GreeterActor {
fn _say_hi(&mut self, args: Box<dyn std::any::Any + Send>) -> Result<Box<dyn std::any::Any + Send>, ActorError>;
}
// Implements the methods on the actor for this specific type
impl GreeterActor for Actor<Greeter>
{
fn _say_hi(&mut self, args: Box<dyn std::any::Any + Send>) -> Result<Box<dyn std::any::Any + Send>, ActorError> {
let name: String = *args.downcast().unwrap();
// This call is the actual execution of the method from the user-defined impl block, on the struct held by the actor
let result: String = self.inner.say_hi(name);
Ok(Box::new(result))
}
}
#[tokio::main]
async fn main() {
let handle = Handle::new(Greeter {});
let greeting = handle.say_hi("Alfred".to_string()).await.unwrap();
assert_eq!(greeting, "hi Alfred".to_string())
}
§Async functions in impl blocks
Async function are fully supported, and work as you would expect:
#[actify]
impl AsyncGreeter {
async fn async_hi(&self, name: String) -> String {
format!("hi {}", name)
}
}
#[tokio::main]
async fn main() {
let handle = Handle::new(AsyncGreeter {});
let greeting = handle.async_hi("Alfred".to_string()).await.unwrap();
assert_eq!(greeting, "hi Alfred".to_string())
}
§Generics in the actor type
Generics in the actor type are fully supported, as long as they implement Clone, Debug, Send, Sync and ’static:
struct GenericGreeter<T> {
inner: T
}
#[actify]
impl<T> GenericGreeter<T>
where
T: Clone + Debug + Send + Sync + 'static,
{
async fn generic_hi(&self, name: String) -> String {
format!("hi {} from {:?}", name, self.inner)
}
}
#[tokio::main]
async fn main() {
let handle = Handle::new(GenericGreeter { inner: usize::default() });
let greeting = handle.generic_hi("Alfred".to_string()).await.unwrap();
assert_eq!(greeting, "hi Alfred from 0".to_string())
}
§Generics in the method arguments
Unfortunately, passing generics by arguments is not yet supported. It is technically possible, and will be added in the near future.
#[actify]
impl Greeter
{
async fn generic_hi<F>(&self, name: String, f: F) -> String
where
F: Debug + Send + Sync,
{
format!("hi {} with {:?}", name, f)
}
}
TODO: show temporary workaround with PhantomData in the actor struct
§Passing arguments by reference
As referenced arguments cannot be send to the actor, they are forbidden. All arguments must be owned:
#[actify::actify]
impl MyActor {
fn foo(&self, forbidden_reference: &usize) {
println!("Hello foo: {}", forbidden_reference);
}
}
§Standard actor methods
TODO: add documentation on the standard actor methods like get and set
§Atomicity
TODO: add documentation on how to guarantee atomocity by preventing updating actors with gets and sets
§Preventing cycles
TODO: add documentation on how to prevent cycles when actors use eachothers handles
Structs§
- A simple caching struct that can be used to locally maintain a synchronized state with an actor
- A clonable handle that can be used to remotely execute a closure on the corresponding actor
- The throttle builder helps build a throttle to execute a method on the current state of an actor, based on the
Frequency
.
Enums§
- The Frequency is used to tune the speed of the throttle.
Traits§
- An implementation of the ActorOption extension trait for for the standard
Option
. This extension trait is made available on the handle through the actify macro. Within the actor these methods are invoken, which in turn just extend the functionality provides by the std library. - The Throttled trait can be implemented to parse the type held by the actor to a custom output type. This allows a single
Handle
to attach itself to multiple throttles, each with a seperate parsing implementation.
Attribute Macros§
- The actify macro expands an impl block of a rust struct to support usage in an actor model. Effectively, this macro allows to remotely call an actor method through a handle. By using traits, the methods on the handle have the same signatures, so that type checking is enforced