What is the idiomatic way to add type alias with lifetime param?

I defined an Error enum

#[derive(Fail, Debug)]
pub enum Error<'a> {
    #[fail(display = "InvalidParams")]
    InvalidParams,

    #[fail(display = "MissingField")]
    MissingField(&'a str),

    #[fail(display = "InvalidParams")]
    FailedParsing,

    #[fail(display = "MailboxError")]
    MailboxError,

    #[fail(display = "R2d2Error")]
    R2d2Error,

    #[fail(display = "FailedRetrieveSequenceId")]
    FailedRetrieveSequenceId,

    #[fail(display = "AlreadySignedUp")]
    AlreadySignedUp,
}

Since I'm using Actic-web framework, I need define a Message for accessing database

impl Message for User {
    type Result = Result<User, Error<'a>>;
}

for resolving undeclared lifetime issue above, I tried add 'a lifetime to User struct

impl<'a> Message for User<'a> {
    type Result = Result<User, Error<'a>>;
}

but User don't need a lifetime anymore

pub struct User {
    pub id: i64,
    pub email: String,
    pub password: String,
    pub nickname: String,
    pub created_at: NaiveDateTime,
}

then tried other 2 approachs, none of them work...

You’re talking about an associated type here, not a type alias - just wanted to make sure you have the right terminology in place in case you want to do some additional research.

In all likelihood, you can get away with:

type Result = Result<User, Error<'static>>;

Putting things like lazy_static aside, this basically means you’ll know, at compile time, the name of the missing field to use for the MissingField variant (if you ever construct it).

Note also that the Message trait requires that Result: 'static. This means you have no choice but to use a static lifetime if your associated type has a lifetime parameter.

But, all that said, you likely just want to make MissingField(String) - ie replace the reference with an owned string and then drop the lifetime parameter from Error. Did you have a reason for wanting a string slice?

Separately, you probably want #[fail(display = "Missing field: {}", _0)] - this will include the actual field value in the message and makes for better debugging/display experience.

You’re talking about an associated type here, not a type alias, Thank you for correcting me.

Did you have a reason for wanting a string slice?
I was assuming that none 'static lifetime variants will be disposed somewhere when I didn't need them anytime (impl Drop trait). This is just my guessing based on Java and Go (GC language)
I haven't learnt Rust memory model
Maybe for modern computers, these tiny strings or objects won't make program inefficient anymore :slight_smile:

I’d suggest starting your Rust foray with reading the free book: Foreword - The Rust Programming Language. There’s a section dedicated to ownership, but read the whole book without skipping anything :slight_smile: - it won’t make you a Rust expert but will make the learning experience quite a bit smoother.

On a side note, I'd also like to suggest to not use &'a str as the field of Error::MissingField.

I've found that the thing you most often want to do with an error is bubble it up. However, lifetimes put a heavy restriction on that: you can't bubble an error past the scope of the variable that you're borrowing the field name from!

Since errors often are something rare and performance isn't a concern when handling them, it's IMO easier to just use a String here. An additional bonus is that you can avoid dealing with lifetimes in your case. Your type will simply be:

type Result = Result<User, Error>;
1 Like

Yes, indeed, you're totally right.
I think I was over design for tiny effciency

Note that it is possible to have a type alias with a lifetime parameter:

type Result<'a> = std::result::Result<User, Error<'a>>;

Note that you have to declare the lifetime parameter before using it. You also must disambiguate the Result identifier because otherwise it will be a cyclic definition. While you can do stuff like let x = x.unwrap(); in variable declarations, it doesn't work with type alias declarations.

1 Like

Emmm..... I tried that and worked!

but if I put that line in Message implementation

impl Message for Save {
    type Result<'a> = Result<User, test::TestError<'a>>;
}

I got error[E0658]: generic associated types are unstable (see issue #44265)

this error is kinda misleading..

This ties in to what @vitalyd said earlier:

While type aliases and associated types both use the type keyword, they're not quite the same thing. If you see a type directly inside an impl block, that's an associated type. Defining type parameters/lifetimes on associated types isn't a stable feature yet, hence the error you're getting - that said, I don't think that's actually what you're trying to do here!

1 Like

To make things a little clearer, let's boil things down to the minimum possible example:

trait Trait {
    type AssocType;
}

struct Struct;
struct StructWithLifetime<'a>;

impl Trait for Struct {
    type AssocType = StructWithLifetime<'a>;
}

If you try to run this code, you'll get an error saying the lifetime is in impl Trait for Struct is undefined - in the context of that implementation, the compiler does not know what lifetime 'a is. So we add a lifetime definition:

impl<'a> Trait for Struct {
    type AssocType = StructWithLifetime<'a>;
}

Now we get a slightly different error - it says that 'a is an 'unconstrained' lifetime. The compiler knows that 'a is the lifetime of StructWithLifetime, but it has nothing to compare that to, so it can't determine exactly how long that struct needs to live.

So we need to add another lifetime annotation to the implementation block - but where? We can't put it on Trait, because Trait isn't defined as having any lifetime parameters in its original definition. The same goes for Struct. If generic associated types were stable and Trait defined AssocType as having a lifetime, then we could put it there, but it doesn't, so we can't.

That's the real issue with your original code - there are no lifetime parameters on any of the other elements of the trait implementation (Message, User and Result) that you can tie 'a to, so the compiler has no context with which to figure out how long 'a actually is. The only lifetime you can feasibly use is 'static, which will always be the lifetime of the whole program, no matter what the context. That's a pretty big limitation, hence why people are recommending not to use lifetimes here :slight_smile:

5 Likes

Beautiful!!!:+1:
Thanks a lot

1 Like