Converting a trait to an owned implementation of itself?

I'm sorry the title is confusing and perhaps that is indication that my intended design is not ideal.

I'm working on a crate for JSON Schema. As part of my core crate, I have a trait that represents the output of the evaluation (named Report). In almost all cases, the Report borrows from the input serde_json::Value, except in the case of a compilation error. In that scenario, the Report needs to be owned.

Given that there's no way to indicate Self<'static>, I'm using an associated type, Owned, to fill in for the owned flavor, returned from the method into_owned:

pub trait Report<'v>: Clone + std::error::Error + Serialize + DeserializeOwned {
    type Error<'e>: 'e + Serialize + DeserializeOwned;
    type Annotation<'a>: 'a + Serialize + DeserializeOwned;
    type Output: 'static + Output;
    type Owned: 'static
        + Report<
            'static,
            Error<'static> = Self::Error<'static>,
            Annotation<'static> = Self::Annotation<'static>,
            Output = Self::Output,
        >;
    fn into_owned(self) -> Self::Owned;
   // snip
}

However, this doesn't work, as the compiler believes the referenced value is leaking:

    fn validate<'v>(
        &mut self,
        dialect_idx: usize,
        value: &'v Value,
    ) -> Result<(), CompileError<C, K>> {
        if !self.validate {
            return Ok(());
        }
        let mut evaluated = HashSet::default();
        let mut eval_numbers = Numbers::with_capacity(7);
        let key = self.dialects.get_by_index(dialect_idx).unwrap().schema_key;

        let report = self.schemas.evaluate(Evaluate {
            key,
            value,
            criterion: &self.criterion,
            output: <CriterionReportOutput<C, K>>::verbose(),
            instance_location: Pointer::default(),
            keyword_location: Pointer::default(),
            sources: self.sources,
            evaluated: &mut evaluated,
            global_numbers: self.numbers,
            eval_numbers: &mut eval_numbers,
        })?;
        if !report.is_valid() {
            let report: <C::Report<'v> as Report>::Owned = report.into_owned();
            return Err(CompileError::SchemaInvalid {
                report,
                backtrace: Backtrace::capture(),
            });
        }
        Ok(())
    }
}
error: lifetime may not live long enough
   --> grill-core/src/schema/compiler.rs:732:25
    |
707 |     fn validate<'v>(
    |                 -- lifetime `'v` defined here
...
732 |             let report: <C::Report<'v> as Report>::Owned = report.into_owned();
    |                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ type annotation requires that `'v` must outlive `'static`

I have a repro here: Rust Playground

Is this possible? Is there anyway to accomplish this without re-designing everything? I doubt anyone wants to look through the source but its available here: GitHub - chanced/grill at initial-version

Thanks,
Chance

In your playground, from spike's perspective, all it knows is that the output type of fake_report implements Report<'v>. Let's call that type FakeOut. When you call .into_owned() on the returned value, you get a <FakeOut as Report<'v>>::Owned, which could be anything that meets the associated type bounds.

You could make fake_report more specific...

//                                                vvvvvvvvvvvvvvvvvvvvv
fn fake_report<'v>(s: &'v str) -> impl Report<'v, Owned = Impl<'static>> {

...and that compiles. But I don't think this is the same as the problem you're dealing with in the rest of your OP.


What's the wider context of your code snippet? It may be useful to see where everything like C and K are defined. <C::Report<'v> as _> implies Report<'_> is a GAT (and not just a trait), but I'm not seeing such a GAT in your code.

If there's more to the error as reported by cargo check, that may also be useful.

Hey @quinedot , thank you for spending some of your valuable time to help! I really appreciate it.

Yea, that repro may not align then. I'm sorry about that. Also, that's all of the error message I'm getting.

Here are the full definitions:

Criterion

pub trait Criterion<K: Key>: Sized + Clone + Debug {
    type Context;
    type Compile: 'static;
    type Keyword: Keyword<Self, K>;
    type Report<'v>: Report<'v>;

    /// Creates a new context for the given `params`.
    fn context(&self, params: Context<Self, K>) -> Self::Context;

    /// Creates a new `Self::Compile`
    fn compile(&mut self, params: Compile<Self, K>) -> Self::Compile;
}

Report

pub trait Report<'v>: Clone + std::error::Error + Serialize + DeserializeOwned {
    type Error<'e>: 'e + Serialize + DeserializeOwned;
    type Annotation<'a>: 'a + Serialize + DeserializeOwned;
    type Output: 'static + Output;
    type Owned: 'static
        + Report<
            'static,
            Error<'static> = Self::Error<'static>,
            Annotation<'static> = Self::Annotation<'static>,
            Output = Self::Output,
        >;
    fn into_owned(self) -> Self::Owned;

    fn new(
        output: Self::Output,
        absolute_keyword_location: AbsoluteUri,
        keyword_location: Pointer,
        instance_location: Pointer,
        assessment: Assessment<Self::Annotation<'v>, Self::Error<'v>>,
        is_transient: bool,
    ) -> Self;

    fn is_valid(&self) -> bool;
    fn append(&mut self, nodes: impl Iterator<Item = Self>);
    fn push(&mut self, output: Self);
}

Compiler

impl<'i, C, K> Compiler<'i, C, K>
where
    C: Criterion<K>,
    K: Key,
{
    fn validate<'v>(
        &mut self,
        dialect_idx: usize,
        value: &'v Value,
    ) -> Result<(), CompileError<C, K>> {
        if !self.validate {
            return Ok(());
        }
        let mut evaluated = HashSet::default();
        let mut eval_numbers = Numbers::with_capacity(7);
        let key = self.dialects.get_by_index(dialect_idx).unwrap().schema_key;

        let report = self.schemas.evaluate(Evaluate {
            key,
            value,
            criterion: &self.criterion,
            output: <CriterionReportOutput<C, K>>::verbose(),
            instance_location: Pointer::default(),
            keyword_location: Pointer::default(),
            sources: self.sources,
            evaluated: &mut evaluated,
            global_numbers: self.numbers,
            eval_numbers: &mut eval_numbers,
        })?;
        if !report.is_valid() {
            let report: <C::Report<'v> as Report>::Owned = report.into_owned();
            return Err(CompileError::SchemaInvalid {
                report,
                backtrace: Backtrace::capture(),
            });
        }
        // TODO: remove the above if statement and replace with the below once fixed
        // ensure!(
        //     report.is_valid(),
        //     SchemaInvalidSnafu {
        //         report: report.into_owned()
        //     }
        // );
        Ok(())
    }
}

This repro seems to be more inline: Rust Playground

use std::{borrow::Cow, marker::PhantomData};

trait Report<'v>: Sized {
    type Owned: 'static + Report<'static>;
    fn into_owned(self) -> Self::Owned;

    fn fake(s: &'v str) -> Self;
}

struct ReportImpl<'v> {
    data: Cow<'v, str>,
}

impl<'v> Report<'v> for ReportImpl<'v> {
    type Owned = ReportImpl<'static>;

    fn into_owned(self) -> Self::Owned {
        ReportImpl {
            data: Cow::Owned(self.data.into_owned()),
        }
    }

    fn fake(s: &'v str) -> Self {
        ReportImpl {
            data: Cow::Borrowed(s),
        }
    }
}

trait Criterion {
    type Report<'v>: Report<'v>;
}

struct CriterionImpl {}

impl Criterion for CriterionImpl {
    type Report<'v> = ReportImpl<'v>;
}

struct Error<C: Criterion> {
    report: <<C as Criterion>::Report<'static> as Report<'static>>::Owned,
}

struct Compiler<C: Criterion> {
    criterion: PhantomData<C>,
}

impl<C: Criterion> Compiler<C> {
    fn compile(&self) -> Result<(), Error<C>> {
        let s = "not static".to_string();
        let report = C::Report::fake(&s);
        Err(Error {
            report: report.into_owned(),
        })
    }
}

The problem is that as far as the type system is concerned, nothing requires these two types to be the same:

<<C as Criterion>::Report<'local > as Report<'local >>::Owned
<<C as Criterion>::Report<'static> as Report<'static>>::Owned

You can say that all the Owned associated types have to be the same by putting this on the implementation block:

impl<C: Criterion, O> Compiler<C>
where
    for<'a> <C as Criterion>::Report<'a>: Report<'a, Owned = O>,
{

And that compiles. But you'll need that bound everywhere you need to utilize it.


It's possible to phrase that bound in a way that compiles on the associated type:

type Owned: 'static + Report<'static, Owned = <Self as Report<'v>>::Owned>;

But the compiler still thinks it needs to call a method of Report<'static> I think, which I haven't found a way around yet.

On a higher level, this is probably getting "in the way" sometimes:

struct Error<C: Criterion> {
    report: <<C as Criterion>::Report<'static> as Report<'static>>::Owned,
}

Namely when you have a C::Report<'non_static>.

If I come up with something potentially less cumbersome I'll let you know.

You can put the equality constraint into Criterion instead.

trait Criterion {
    type Owned: 'static + Report<'static>;
    type Report<'v>: Report<'v, Owned = <Self as Criterion>::Owned>;
}

struct CriterionImpl {}
impl Criterion for CriterionImpl {
    type Owned = <ReportImpl<'static> as Report<'static>>::Owned;
    type Report<'v> = ReportImpl<'v>;
}

struct Error<C: Criterion> {
    report: <C as Criterion>::Owned,
}

(It might be better to give it a distinct name to reduce ambiguity.)

1 Like

@quinedot you are truly an awesome. Thank you!

That's brilliant. I would not have come up with that. Thank you again!

Hey, I'm not sure if you are on stack or not, but I had posted it there as well: rust - Converting a trait to an owned implementation of itself? - Stack Overflow

Sorry, I forgot to mention it earlier. It was amongst the tabs I was closing for the night.

Fill free to provide the solution on stack yourself.

Alright, I'll copy over and link your reply. Figured I'd let you know incase rep there mattered to you.

Thanks again.

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.