Help with generics, lifetimes, and async (tokio)

Hello, I've been working on a small library based on the ethers crate. My code is up at Github.

There are two branches: master and generify. The code is exactly the same, except generify is one commit ahead, and that one commit is using generic types to get the code ready for testing with mocks. There are two places where the generify code doesn't compile and I'd like some help understanding why that is.


The first error occurs at lines 244 and 245 in Here's a screenshot as well:

The compiler wants me to add the 'static lifetime bounds to M and M::Error and I don't understand why. For comparison, the equivalent version from master branch without generics has no issues:


The second error occurs at the task::spawn() call at line 157 in Here's a screenshot as well for reference:

Here the compiler wants me to add the 'static lifetime bound to all my generic parameters and I don't understand why that is necessary if all the arguments are owned types. Similarly here's the master version that works.

I know it's asking for a lot but would appreciate any help understanding what's exactly going on. Just doing what the compiler wants without understanding it doesn't seem satisfactory.

Probably because eyre::Error requires 'static (likely for downcasting).

It's exactly the 'static bound that means that the type parameters are owned types. A type parameter in itself can stand for a reference type as well when instantiated, so it's not necessarily an owned type.

Hi @H2CO3 thanks for taking the time to read. Sorry but I don't quite follow what you mean re: eyre::Error requiring 'static and how that relates to the first error I described. I changed the function to use Box<dyn Error> and the issues still persist. Maybe I misunderstood you? :pray:

Box<dyn Error> is still shorthand for Box<dyn Error + 'static>. It's fine for you to use eyre::Error; the right solution is likely to add the 'static bound to your error type. Error types should usually be 'static so that they can support downcasting.

Re: the above, I just want to make sure I understand. The function in question has the following signature:

async fn send_transaction<B, M, D, S>(
    signer: Arc<SignerMiddleware<Arc<M>, S>>,
    bcast_resp_sender: mpsc::Sender<BroadcastResponse>,
    tx_with_internal_nonce: TXWithInternalNonce<B, M, D>,
) where
    B: Borrow<M> + Send + Sync,
    M: Middleware,
    D: Detokenize + Send + Sync,
    S: Signer,

And we try to pass this as an argument to tokio::task::spawn() as follows:

        let signer = Arc::clone(signer);
        let bcast_resp_sender = bcast_resp_sender.clone();

So you're saying that even though I know I'm passing owned types, the compiler will assume that B, M, D, and S could very well be reference types (even though they are all Send and Sync and one of them is wrapped inside an Arc). So by adding the 'static lifetime bound to all the generic types, the compiler can rest assured that those will indeed be owned types (and not reference types). Did I understand correctly? Thank you!

It's not merely an assumption; they can in fact be reference types. It's perfectly possible to create an Arc<&T>, for example, albeit strange/mostly useless. But nothing – except trait bounds – prevents the arbitrary composition of types; there's no structural reason why an Arc couldn't contain a reference or a Box or another Arc or even a raw pointer.

Rust generics are not C++ templates. Generic items are type-checked once, when defined. It is not the case that every individual instantiation of a generic item is separately type-checked. Therefore, the implementation is only allowed to rely on properties that every possible instantiation can guarantee.


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.