Rust: implement trait for the associated type

1k views Asked by At

In the snippet below (https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=5babd9991122c756e7afaa76da0b32f3)

#![feature(generic_associated_types)]
#![feature(marker_trait_attr)]

// StrSpan is opaque and imported from nom_locate
struct StrSpan<'a>(&'a str);

pub trait LifetimizedExt {
    type Lifetimized<'a>;
}

impl<T> LifetimizedExt for Option<T>
where
    T: LifetimizedExt
{
    type Lifetimized<'a> = Option<T::Lifetimized<'a>>;
}

impl<'a,T> LifetimizedExt for &'a T
where
    T: 'static + ?Sized
{
    type Lifetimized<'b> = &'b T;
}

impl<'a,T> LifetimizedExt for &'a mut T
where
    T: 'static + ?Sized
{
    type Lifetimized<'b> = &'b mut T;
}

#[marker]
pub trait ParsableInput: LifetimizedExt {}

impl<'a> ParsableInput for &'a str {}
impl<'a> ParsableInput for StrSpan<'a> {}

impl<'a,I> ParsableInput for I::Lifetimized<'a>
where
    I: ParsableInput
{}

specifically in

impl<'a,I> ParsableInput for I::Lifetimized<'a>
where
    I: ParsableInput
{}

I is unconstrained.

According to https://doc.rust-lang.org/error-index.html#E0207,

Any type parameter of an impl must meet at least one of the following criteria:

  • it appears in the implementing type of the impl, e.g. impl Foo for a trait impl,
  • it appears in the implemented trait, e.g. impl SomeTrait > for Foo
  • it is bound as an associated type, e.g. impl<T, U> SomeTrait for T where T: AnotherTrait<AssocType=U>

How do I rewrite the original snippet?

I always want to use ParsableInput trait to infer that I::Lifetimized<'a> is ParsableInput for any lifetime 'a. Is it possible?

Requested addition.

The reasons why such behavior is desired are that

  1. It's just stating the important fact about the concerned types
  2. Nom combinators require specific type signatures for functions to be used as their arguments.

They are generic as well. For example, line_ending. It requires T: InputIter + InputLength.

Without generic associated types, there will be too much repetition, and even one small combinatorial explosion (NxM implementations).

pub trait Parse<'a,I>: Sized
where
    I: ParsableInput,
    Self: LifetimizedExt<Lifetimized<'a> = Self>
{
    /// Returns [`Result<P,E>`] where any [`Ok(p)`] is a pair `(i,val)`, s.t.
    /// * `i` is the remaining input after parsing
    /// * `val` is the parsed value
    fn parse<'b, 'c>(i: I::Lifetimized<'b>) -> IResult<I::Lifetimized<'c>, Self::Lifetimized<'a>>
    where
        'b: 'c,
        'b: 'a;
}

Addition #2: "Why LifetimizedExt exists"

Its associated generic type Lifetimized is the reification of the class of types equivalent to Self up to lifetime. I needed to make implementations of Parse::parse for (&'b str,&'c str) and for (StrSpan<'b>,StrSpan<'c>) (modulo other details). Theoretically, I could supply a pair-type-argument but (sic) parse wouldn't be generic.

Note: Now I realize that it could be generic if the constructed triple (&'a str,&'b str,&'c str) satisfied some trait but it would make things even more complicated.

Addition #3: "Why was genericity in Parse::parse needed"

There also was map_parsed_val function,

// Proper declaration and implementation requires GATs

use nom::IResult;

pub trait MapParsedValInTuple<'a, T>
where
    for<'c> T: super::Parse<'a,&'c str>,
{
    fn map_parsed_val<'b, U, F: FnOnce(T) -> U>(self, f: F) -> (&'b str, U)
    where
        'a: 'b;
}

impl<'a, T> MapParsedValInTuple<'a, T> for (&'a str, T)
where
    for<'c> T: super::Parse<'a,&'c str>,
{
    fn map_parsed_val<'b, U, F: FnOnce(T) -> U>(self, f: F) -> (&'b str, U)
    where
        'a: 'b,
    {
        let (remaining_input,parsed_val) = (self.0, self.1);
        (remaining_input, f(parsed_val))
    }
}

pub trait MapParsedValInResult<'a, T> {
    fn map_parsed_val<'b, U, F: FnOnce(T) -> U>(self, f: F) -> IResult<&'b str, U>
    where
        'a: 'b;
}

impl<'a, T> MapParsedValInResult<'a, T> for IResult<&'a str, T> {
    fn map_parsed_val<'b, U, F: FnOnce(T) -> U>(self, f: F) -> IResult<&'b str, U>
    where
        'a: 'b,
    {
        self.map(|(i, parsed_val)| (i, f(parsed_val)))
    }
}

I haven't rewritten it yet but you can see that GATs would be nifty here. Also, you can notice that it deals only with types whose type-parameter for Parse is &str. With the addition of StrSpan, in this place too would happen combinatorial explosion. It's also important to note that it ends life of the parsed value while keeping the lifetime of the remaining input intact. How long they will live, how the function will be used, is unknown (and irrelevant as long as genericity in the method is there). It also makes sense that this method should be generic.

0

There are 0 answers