Unable to bind lifetime of a generic to Self in a trait

I am trying to create an alternative version of the Borrow trait that could be used to obtain a smart reference concrete type. Something like the following:

pub trait Borrow<Borrowed>
where
    Borrowed: ?Sized,
{
    fn borrow(&self) -> Borrowed;
}

However, this code has a huge downside: the generic Borrowed object is not bound to the lifetime of Self. I tried different approaches, trying to have a lifetime in the trait definition, as a HRTB and as a function generic parameter, but I did not success to express my intent. To be honest, given that most of the time people ask for help because code does not compile, it is pretty strange to ask how can I make it not compile :sweat_smile:

In any case: here a playground with a not working code and two helpful doctests to check if the goal is reached.

EDIT: updated playground, mis-copy-pasted.


If you are asking "why????", my idea is pretty simple and practical: it happens to use serde in order to deserialize data using borrowing structs, something like this:

#[derive(Deserialize, Serialize)]
struct A<'a> {
    param1: &'a str,
    param2: &'a str,
    #[serde(borrowed)]
    child: B<'a>,
}

#[derive(Deserialize, Serialize)]
struct B<'a> {
    param1: &'a str,
    param2: &'a str,
}

If you need a JSON response just to save some data to a DB, for instance, allocating Strings all around is just a waste of resources.

However, you often reach the point you need the owning version of your structures. AFAIK, at today you need to copy-paste everything and replace all the references with owning version of your data. I think it would be great to create a derive-macro to automatize this boilerplate process, but a trait is necessary in order to allow the parent struct to get the owned version of the child one, and ToOwned cannot be used.

If I am correct, the Borrow trait I am trying to implement should be 100% compatible with the one in std::borrow, because a reference to a generic type is a subset of a generic type, if this obviously has a correct lifetime bound.

/// Without GATs, you need to infect the trait with the lifetime used in the method:
trait Borrow<'lt, Borrowed : ?Sized> : 'lt {
    fn borrow (self: &'lt Self) -> Borrowed
    ;
}

// An example
impl<'lt, T, U> Borrow<'lt, &'lt U> for T
where
    U : 'lt,
    T : 'lt + ::std::borrow::Borrow<U>,
{
    fn borrow (self: &'lt T) -> &'lt U
    {
        ::std::borrow::Borrow::borrow(self)
    }
}

I’m having trouble understanding the connection between

  • the proposed Borrow trait being for “smart references” (whatever those even are)

and

  • a trait that’s “necessary” for a derive-macro to automate a boilerplate process of creating a copy of a struct hierarchy where &'a str is replaced by String, together with some conversion functions.

Unfortunately the code that should not compile, compiles :frowning_face:

Sorry, I only saw the beginning of the post, it reminded me of my having already tackled that issue, and so I started writing and failed to see that you had already tried my suggestion exactly :sweat_smile:

Your compile-fail test fails to fail because despite the trait signature, Rust sees the concrete type signatures, and knows that ARef is not bounded by 'lt.

Should you have been using generic params bounded by your/mine Borrow trait, then it wouldn't have compiled, even when instanced with your example.


That being said, if you are not writing unsafe code, wanting code not to compile is a quite rare thing :smile:

1 Like

I try to explain it in a more clear way using the previous example.

Imagine what the proc-macro should do when analyzing A. &str can be replaced with String, because 1. is a known type 2. ToOwned can be used.

However, when it reaches the field child, it cannot know a priori how to create an owned version, therefore it needs a trait similar to ToOwned to convert B to something like OwnedB. Unfortunately, ToOwned: Borrow, and Borrow::borrow -> &Borrowed, which means that OwnedB::borrow(&self) -> &Something, but in our case we want OwnedB::borrow(&self) -> B<'_>.

I hope that now it is a bit clearer, it is a bit complicated, I know.

:heart:

Exactly. In fact, I am not completely sure that something like that can be expressed without strange shenanigans (like allow implementing the trait only through a proc-macro). I hope it is, but at the same time what I would like to avoid is a logic error, not a safety issue, therefore I think I am trying to ask a bit too much to the type system.

1 Like

You could have an intermediate lifetime-carrying struct:

Example
use core::marker::PhantomData;

pub trait BorrowRef<'a, Borrowed>
where
    Borrowed: ?Sized,
{
    fn borrow(self: &'a Self) -> LimitedBorrow<'a, Self, Borrowed>;
}

#[repr(transparent)]
pub struct LimitedBorrow<'a, T: ?Sized, U: ?Sized> {
    guard: PhantomData<&'a T>,
    inner: U,
}

// impl `Deref`, etc, on `LimitedBorrow`

// ...
    struct ARef;
    impl<'a> BorrowRef<'a, ARef> for A {
        fn borrow(&self) -> LimitedBorrow<'a, Self, ARef> {
            LimitedBorrow { guard: PhantomData, inner: ARef }
        }
    }
1 Like

Holy moly, that's a really good idea! It is simple, you just focused on the return type instead of the trait bounds!

Thank you so much, you also gave me some ideas-- I just tried one, but unfortunately it does not work. I wanted to try using an intermediate lifetime-carrying trait instead of a struct, combined with the ability of giving a default associated type in nightly. Something like the following:

pub trait BorrowRef<'a, Borrowed>
where
    Borrowed: Bound<'a>,
{
    fn borrow(self: &'a Self) -> Borrowed::Ty;
}

pub trait Bound<'a>: 'a + Sized {
    type Ty = Self;
}

impl<'a, T> Bound<'a> for T
where
    T: 'static
{
    type Ty = RefBound<'a, T>;
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct RefBound<'a, T> {
    _marker: PhantomData<&'a T>,
    inner: T,
}

impl<'a, T> From<T> for RefBound<'a, T> {
    fn from(t: T) -> Self {
        Self {
            inner: t,
            _marker: PhantomData,
        }
    }
}

As said, this does not work, because I wanted to specialize the default associated type, but it's not how the feature works. I could try something directly related with specialization, but to be honest I never played enough with it to understand if this is feasible.

For now, and without any other idea, your solution is the best for now :wink:. Thanks again!

1 Like