Crate actify

source ·
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§

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