How to eliminate a lifetime that is known to strictly not outlive the type itself?

Hi forum, first time poster here.

Say I have a Notify trait:

trait Notify {
    type Message;
    type Error;
    fn notify(&self, message: Self::Message) -> Result<(), Self::Error>;
}

and a Notifier type that provides generic implementation for anything that is Serialize:

struct Notifier<M>(__phantom: PhantomData<fn(M)>);

impl<M: Serialize> Notify for  Notifier<M> {
    type Message = M;
    type Error = SomeErrorType;
    fn notify(&self, message: Self::Message) -> Result<(), Self::Error> {
        /* implementation... */
    }
}

I want to define a concrete type EmailNotifier that accepts the email body as string, and sends it by delegating to the generic implementation:

#[derive(Serialize)]
struct EmailPayload<'a> {
    sender: &'a str,
    receiver: &'a str,
    message: String,
}

struct EmailNotifier<'a> {
    sender: String,
    receiver: String,
    notifier: Notifier<EmailPayload<'a>>
};

impl<'a> Notify for EmailNotifier<'a> {
    type Message = String;
    type Error = SomeErrorType;
    fn notify(&self, message: Self::Message) -> Result<(), Self:: Error> {
        let payload = EmailPayload {
            sender: &self.sender,
            receiver: &self.receiver,
            message,
        };
        self.notifier.notify(payload)
    }
}

This code works, but as you can see the type carries a lifetime type parameter 'a, which is strictly shorter than the borrow of self. Can I convey this information to the compiler so that EmailNotifier does not need to carry this lifetime parameter, as it leaks internal implementation details?

Thank you for taking time to read through the post, any help is appreciated.

EDIT: also assume the error type is 'static so it does not use borrows from the payload.

Why do you use &str for the sender and receiver, as opposed to using String?

To avoid cloning the strings. It's intended to reuse the EmailNotifier to send multiple notifications.

I understand. You are probably looking for something like this:

use serde::Serialize;

struct SomeErrorType;

trait Notify<M> {
    type Error;
    fn notify(&self, message: M) -> Result<(), Self::Error>;
}

struct Notifier;

impl<M: Serialize> Notify<M> for Notifier {
    type Error = SomeErrorType;
    fn notify(&self, message: M) -> Result<(), Self::Error> {
        todo!()
    }
}


#[derive(Serialize)]
struct EmailPayload<'a> {
    sender: &'a str,
    receiver: &'a str,
    message: String,
}

struct EmailNotifier {
    sender: String,
    receiver: String,
    notifier: Notifier,
}

impl Notify<String> for EmailNotifier {
    type Error = SomeErrorType;
    fn notify(&self, message: String) -> Result<(), Self:: Error> {
        let payload = EmailPayload {
            sender: &self.sender,
            receiver: &self.receiver,
            message,
        };
        self.notifier.notify(payload)
    }
}

Why not build the Notifier<EmailPayload<'_>> inside each EmailNotifier::notify call?

Thanks, that does solve the problem! I can get rid of the phantom data as well.

The initial conception of Notify was that a notifier only works with one type of message. Then I find a generic implementation for Serialize types can be abstracted, so the Notifier<M> type. Do you think it's generally a good idea to define traits with generic parameters instead of associated types, as opposed to creating types that are generic implementation for a trait?

In my actual code, the Notifier needs to be initialized with some complex logic, so it's better to be passed in as an argument when constructing the EmailNotifier instance.

The difference between a generic and associated type is whether the trait can be implemented multiple times with different choices of the type. In your case, you want the same Notifier to implement Notify with many different types, including EmailNotifier<'a> and EmailNotifier<'b> and EmailNotifier<'c> and so on, for all possible lifetimes, but if you use an associated type, you can only pick one of them.

1 Like

So to answer this, it depends on whether you want it to be possible to implement the trait multiple times, with each implementation having a different choice for the type, or if you want the trait to be implementable only once, with only one type chosen.

So in my case the Notifier started with only one possible Message type to deal with. When it should work with multiple types, it's better to refactor the Notify trait to be implementable for multiple types, as it indicates that the restriction (one message type per Notify type) imposed by the trait is no longer appropriate, right?

I mean we could discuss whether that restriction was ever appropriate to start with, but other than that, yes.

I got it. Thank you!

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.