#asynchronous #holo #holochain #actor

ghost_actor

GhostActor makes it simple, ergonomic, and idiomatic to implement async / concurrent code using an Actor model

57 releases (4 breaking)

0.4.0-alpha.5 Jan 18, 2021
0.3.0-alpha.1 Nov 16, 2020
0.2.0 Jul 1, 2020
0.0.42 Mar 10, 2020
0.0.22 Nov 13, 2019

#26 in Asynchronous

Download history 410/week @ 2020-11-07 503/week @ 2020-11-14 296/week @ 2020-11-21 276/week @ 2020-11-28 353/week @ 2020-12-05 377/week @ 2020-12-12 313/week @ 2020-12-19 68/week @ 2020-12-26 328/week @ 2021-01-02 267/week @ 2021-01-09 270/week @ 2021-01-16 245/week @ 2021-01-23 230/week @ 2021-01-30 344/week @ 2021-02-06 799/week @ 2021-02-13 287/week @ 2021-02-20

1,279 downloads per month
Used in 3 crates

Apache-2.0

42KB
766 lines

Crates.io Crates.io

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.5–2.1MB
~42K SLoC