62 releases
0.4.0-alpha.5 | Jan 18, 2021 |
---|---|
0.3.0-alpha.6 | Jun 21, 2023 |
0.3.0-alpha.5 | Mar 14, 2023 |
0.3.0-alpha.4 | Sep 8, 2021 |
0.0.22 | Nov 13, 2019 |
#162 in Asynchronous
3,054 downloads per month
Used in 40 crates
(5 directly)
43KB
766 lines
ghost_actor
GhostActor makes it simple, ergonomic, and idiomatic to implement async / concurrent code using an Actor model.
GhostActor uses only safe code, and is futures executor agnostic--use tokio, futures, async-std, whatever you want. The following examples use tokio.
What does it do?
The GhostActor struct is a 'static + Send + Sync
cheaply clone-able
handle for managing rapid, efficient, sequential, mutable access to
internal state data.
Using the raw type:
// set our initial state
let (a, driver) = GhostActor::new(42_u32);
// spawn the driver--using tokio here as an example
tokio::task::spawn(driver);
// invoke some logic on the internal state (just reading here)
let result: Result<u32, GhostError> = a.invoke(|a| Ok(*a)).await;
// assert the result
assert_eq!(42, result.unwrap());
Best Practice: Internal state in a New Type:
GhostActor is easiest to work with when you have an internal state struct, wrapped in a new type of a GhostActor:
struct InnerState {
age: u32,
name: String,
}
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct Person(GhostActor<InnerState>);
impl Person {
pub fn new(age: u32, name: String) -> Self {
let (actor, driver) = GhostActor::new(InnerState { age, name });
tokio::task::spawn(driver);
Self(actor)
}
pub async fn birthday(&self) -> String {
self.0.invoke(|inner| {
inner.age += 1;
let msg = format!(
"Happy birthday {}, you are {} years old.",
inner.name,
inner.age,
);
<Result::<String, GhostError>>::Ok(msg)
}).await.unwrap()
}
}
let bob = Person::new(42, "Bob".to_string());
assert_eq!(
"Happy birthday Bob, you are 43 years old.",
&bob.birthday().await,
);
Using traits (and GhostFuture) to provide dynamic actor types:
pub trait Fruit {
// until async traits are available in rust, you can use GhostFuture
fn eat(&self) -> GhostFuture<String, GhostError>;
// allows implementing clone on BoxFruit
fn box_clone(&self) -> BoxFruit;
}
pub type BoxFruit = Box<dyn Fruit>;
impl Clone for BoxFruit {
fn clone(&self) -> Self {
self.box_clone()
}
}
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct Banana(GhostActor<u32>);
impl Banana {
pub fn new() -> BoxFruit {
let (actor, driver) = GhostActor::new(0);
tokio::task::spawn(driver);
Box::new(Self(actor))
}
}
impl Fruit for Banana {
fn eat(&self) -> GhostFuture<String, GhostError> {
let fut = self.0.invoke(|count| {
*count += 1;
<Result<u32, GhostError>>::Ok(*count)
});
// 'resp()' is a helper function that builds a GhostFuture
// from any other future that has a matching Output.
resp(async move {
Ok(format!("ate {} bananas", fut.await.unwrap()))
})
}
fn box_clone(&self) -> BoxFruit {
Box::new(self.clone())
}
}
// we could implement a similar 'Apple' struct
// that could be interchanged here:
let fruit: BoxFruit = Banana::new();
assert_eq!("ate 1 bananas", &fruit.eat().await.unwrap());
Custom GhostActor error types:
The GhostActor::invoke()
function takes a generic error type.
The only requirement is that it must implement From<GhostError>
:
#[derive(Debug)]
struct MyError;
impl std::error::Error for MyError {}
impl std::fmt::Display for MyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
impl From<GhostError> for MyError {
fn from(_: GhostError) -> Self {
Self
}
}
let (actor, driver) = GhostActor::new(42_u32);
tokio::task::spawn(driver);
assert_eq!(42, actor.invoke(|inner| {
<Result<u32, MyError>>::Ok(*inner)
}).await.unwrap());
Code Examples:
- Bounce:
cargo run --example bounce
Contributing:
This repo uses cargo-task
.
cargo install cargo-task
cargo task
Dependencies
~1–1.2MB
~20K SLoC