Avoiding heap allocations in (some) async trait methods

Exactly (without having known the term "covariance" in that context at that time). I made that (wrong) interpretation due to observing Rust's behavior when calling functions.

AFAIK, Rust is also often described this way. This kind of statement is of course limited. It’s more of a “if you know what you want, you can write it down, tweak it until it compiles and then it usually works”. With lifetimes, people often don’t know what they want at all, because they don’t understand lifetimes. When the approach is, try out random ways of assigning lifetimes until you find the first one that happens to compile, then it’s unlikely that the signature you found actually is the “correct” one. (This is especially true if you didn’t even write any uses of your function yet, how is the compiler supposed to be able to check that your function signature fits both the implementation of the function and the ways you’d like to use it, if the ways you’d like to use it aren’t even in your code yet?)

Maybe you could also view this as a rule of thumb: If you give incorrect type/function signatures, it’s hard to end up with the right thing. The compiler usually prefers to suggest you changing your implementation to fit the signature than to change the signature to fit the implementation. This applies to Haskell, too; but Haskell has the advantage that type signatures are optional – the language is better suited for type inference and the compiler can usually tell you the correct most general type signature to put on your function after you wrote it. Rust can’t do that; on one hand due to some language mechanisms like method resolution necessarily requiring types to already be known; on the other hand because the compiler currently won’t really accept you leaving out function signatures, even temporarily.

I imagine in the future, we could write a function without any lifetime annotations first (which would still result in a little unobtrusive error informing us that we’ll have to put them in eventually), but you can keep coding until all other errors/warning are gone, and then ask the compiler to fill in the lifetime details in function signatures for you in the most general manner possible, taking the function implementation into account.

1 Like

Maybe it's more: "In Rust you make more run-time and more compile-time errors, but the number of compile-time errors is that much bigger that it looks like the compiler catches all errors." :joy:

Not clear what the “more” refers to. Compare Rust with any language that has a weaker type system, e.g. Java, or Python, and there’s clearly way fewer run-time errors you’ll encounter when programming in Rust. Even compared to Haskell, Rust’s standard library prefers to avoid panicking/fallible functions more; in Haskell many operations will throw an exception when Rust would use Result or Option (typical example: head and tail for lists). This can result in fewer run-time errors because you’ll never forget a function can fail. (Another thing: Rust checks patter matches to be exhaustive by default, while Haskell introduces a catch-all case throwing an exception.)

Don't worry, I made a joke. Seriously: I feel like I make less errors in Rust when comparing it to other languages I have been working in (except Haskell, which may be a special case anyway).

(But note that Haskell can't be as efficient as Rust, I think, so it's not really good to compare these here.)

And ever since I started working with Rust, I don't want to go back to any other language with a weaker type system (with the exception of untyped languages, which I find beautiful in their own way).

With my joke I wanted to express that just because a certain language throws a lot of compiler errors to you as a newcomer, that doesn't necessarily mean that language keeps you safe from errors that unfold at runtime.

Anyway, to get back to the original topic of this thread (async trait methods):

I was happy to read the Lang team October update from October 8th, 2021:

  • Async fundamentals update:
    • What is it? Async fn in traits, async drop, async closures
    • Have designated an MVP version of async functions in traits that we intend to stabilize first, and done a lot of exploration on next steps (read up on that in the ever evolving evaluation doc).

So maybe my worries will be solved soon (at least in some regards, as traits with async fn might not be object safe at first).

Really looking forward to it! Rust is awesome!! :smiley:

Safe Rust can't protect you from all errors in your program logic. What it does do is protect you from difficut-to-reason-about classes of temporal-sequencing and memory-use errors that historically have led to malware-exploitable vulnerabilities.

I think this always depends on what you’re comparing agains. Compared to C++, Rust does protect you against UB / memory unsafety; compared to memory-safe languages, that’s not much of a difference. Rust does offer full protection against memory unsafety and data races; beyond that it has a strong type system which doesn’t fully protect against logic errors, yet still catches many, many, many errors at compile time that would become run-time exceptions or misbehavior on other languages, particularly when you compare to dynamically typed languages, e.g. Python or Javascript.

Rust also offers more protection against errors due to non-thread-safe code. Race conditions are still possible in Rust, but only when you’re e.g. explicitly using atomics, there’s no risk of running into errors due to concurrent uage of code that wasn’t meant to be used from multiple threads.

My main point is; unless you’re coming from an unsafe language like C++, memory safety isn’t really anything new; observations like “if it compiles, it runs correctly” are describing what strong typing does to your coding experiance, and they are particulatly valid when comparing to more weakly typed but memory-safe languages, the observation has little to nothing to do with Rust’s memory safety guarantees (though of course the tools that enable memory safety in Rust are also tools that can also help catch other logic bugs).


I just re-read that MVP:

  • No support for dyn
  • […]
  • No ability to bound the resulting futures (e.g., to require that they are Send )
    • […] the [only] limitation is that one cannot write generic code that invokes spawn.
    • Workaround: do the desugaring manually when required, which would give a name for the relevant future.

I personally don't mind the missing dyn support, but if the trait method returns a ?Send future, then this might be a big implication that would lead to "manual desugaring" again.

And that does get pretty ugly, as I pointed out here:

Also things get even more verbose when there are default implementations for trait methods, as I said here:

That is, I have to write:


use std::future::Future;

type DefaultFoo<T: ?Sized> = impl Future<Output = ()>;
trait Bar {
    type Foo: Future<Output = ()> + Send = DefaultFoo<Self>;
    fn foo(&self) -> DefaultFoo<Self> {
        async move {

(And that example doesn't even deal with extra lifetimes yet.)

So considering that I need my futures to be Send (because I want to write generic code, even if I can live without dyn), the currently planned MVP might not help me much, because I'd end up with the same ugly code.

Nonetheless, I appreciate the progress in that matter. Afterall, it's a first step.

As you pointed out, it is possible to avoid heap allocations in async trait methods (using unstable features). However, it pretty much bloats up the syntax, as I said more up in this thread.

Nonetheless, I decided to go that way and to use nameable existential (associated) types. If you're interested, see this post for a use case where each async trait method shouldn't do any unnecessary heap allocations (as I might have a lot of these calls), and how I applied the method.

Thanks again for bringing that up in the first place! :+1: It has been pretty helpful and seems to be usable in practice (if using unstable features is an option).

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.