Ractor is cool, but I've been wondering why it uses the async `async_trait` rather than native async traits. Is it just because it came out before async traits were stabilized, and now code relies on it and it would be a breaking change to migrate?
For context, the `async_trait` crate makes futures from trait methods that are wrapped in `Pin<Box<dyn Future<...>>>`, which means that every async call to a trait method must make a heap allocation. This is currently a necessary thing to do if async trait methods were invoked with dynamic dispatch (through `dyn Actor`), but the `Actor` trait has associated methods, so that is already not generally possible.
I realize that methods on the `Actor` trait return futures that are `Send`, but specifically for an actor framework that feels like a very specific design choice that isn't universally good or necessary. Another design would give let the spawned task that executes the actor's messages exclusive access to the actor (so `handle()` could take `&mut self`).
I've ended up implementing a simple alternative design in my own project (it's not fundamentally very hard, but doesn't have all the features, like supervision) because the per-message heap allocations and internal locking became wasteful for my use case.
> I've been wondering why it uses the async `async_trait` rather than native async traits
From their crate documentation[0]:
> The minimum supported Rust version (MSRV) is 1.64. However if you disable the `async-trait` feature, then you need Rust >= 1.75 due to the native use of `async fn` in traits.
For context, the `async_trait` crate makes futures from trait methods that are wrapped in `Pin<Box<dyn Future<...>>>`, which means that every async call to a trait method must make a heap allocation. This is currently a necessary thing to do if async trait methods were invoked with dynamic dispatch (through `dyn Actor`), but the `Actor` trait has associated methods, so that is already not generally possible.
I realize that methods on the `Actor` trait return futures that are `Send`, but specifically for an actor framework that feels like a very specific design choice that isn't universally good or necessary. Another design would give let the spawned task that executes the actor's messages exclusive access to the actor (so `handle()` could take `&mut self`).
I've ended up implementing a simple alternative design in my own project (it's not fundamentally very hard, but doesn't have all the features, like supervision) because the per-message heap allocations and internal locking became wasteful for my use case.