Constraining an associated type of a generic parameter

Today I've found myself suddenly having to interact with traits with associated types, and I'm not 100% sure how to achieve what I need to achieve. Sorry that this is a bit long, but I'm not sure exactly how much context is needed for what I'm doing to make sense. There's my best-effort TL;DR at the bottom.

Background

I'm parsing a file using the logos crate, and if I encounter a problem, I'd like to pass up a error struct which contains the following information:

struct ParseError
{
    // The problematic token, or None if EOF
    token: Option<String>,

    // Where the token was encountered
    location: Range<usize>,

    // Programmer description, for context
    description: String,
}

To achieve this, I'd like to be able to create an error like so:

ParseError::from_token(lexer, "Something went wrong here")

My proposed function signature looks like:

// 'l is the lifetime that the lexer object has.
// Ctx is the enum type representing the tokens that are acceptable.
// It implements the Logos trait using #[derive(Logos)].
pub fn from_token<'l, Ctx>(lexer: &logos::Lexer<'l, Ctx>, description: &str)
    -> Self
    where Ctx: logos::Logos<'l>,

And the body of the function would look like this:

{
    return Self {
        token: Some(lexer.slice().to_owned()),
        location: lexer.span(),
        description: description.to_owned(),
    };
}

The issue I'm having is that lexer.slice().to_owned() needs quite precise constraints on the types/traits that my from_token() function uses.

First of all, using the naive signature above, the linter gives me an error regarding lexer.slice().to_owned():

the method to_owned exists for associated type <<Ctx as Logos<'_>>::Source
as Source>::Slice<'_>, but its trait bounds were not satisfied
the following trait bounds were not satisfied:
<<Ctx as Logos<'_>>::Source as Source>::Slice<'_>: Clone
which is required by <<Ctx as Logos<'_>>::Source as Source>::Slice<'_>: ToOwned

So there is a Slice type that needs constraining. Looking at the logos crate, the Source trait begins like this:

pub trait Source {
    /// A type this `Source` can be sliced into.
    type Slice<'a>: PartialEq + Eq + Debug
    where
        Self: 'a;

And Slice is later redefined like this:

impl Source for str {
    type Slice<'a> = &'a str;

So I understand that Source::Slice will refer to a &str when the underlying data source is a str.

However, this is where my current understanding reaches its limit. I'm able to eliminate the linter error about the Clone trait by basically copying and pasting the stated type chain as a trait constraint:

where
    Ctx: logos::Logos<'l>,
    <<Ctx as Logos<'l>>::Source as logos::Source>::Slice<'l>: Clone,

But this doesn't fix the whole problem. I now get a different linter error from lexer.slice().to_owned() that I don't know how to resolve:

mismatched types
expected struct String
found associated type <<Ctx as Logos<'_>>::Source as Source>::Slice<'_>
consider constraining the associated type <<Ctx as Logos<'_>>::Source as
Source>::Slice<'_> to String

My educated guess is that, without a constraint on the associated Slice type, the compiler cannot guarantee that the type will have a to_owned() function that returns a String, and therefore that I need some way to constrain the Slice type to be a &str in this function. I can see how the logos crate does this for its Source trait and the various impls of it, but I don't know how to do it locally in my function.

TL;DR

For a trait MyTrait that has an associated type MyTrait::A, how do I constrain MyTrait::A to a concrete type when writing my_function<T>() where T: MyTrait?

Clone isn't the right bound here anyway — &str doesn't implement Clone. The only reason the compiler mentions Clone is because it has the bad habit of telling you about the fact that a Clone impl would get you a free ToOwned impl, even when that isn't relevant. If you wanted to fix that,

where
    Ctx: logos::Logos<'l>,
    <<Ctx as Logos<'l>>::Source as logos::Source>::Slice<'l>: ToOwned<Owned = String>,

= inside <> is also the syntax you use to get the &str equality you want:

where
    Ctx: logos::Logos<'l>,
    <Ctx as Logos<'l>>::Source: logos::Source<Slice<'l> = &'l str>,

It's also possible, since Rust 1.79, to write bounds as well as equality inside the trait <>, which is often clearer and allows you to name traits only once instead of twice:

where
    Ctx: logos::Logos<'l, Source: logos::Source<Slice<'l> = &'l str>>,
2 Likes

Thanks, that last one is super concise and seems to be exactly what I need.

So to make sure I'm understanding, in the general case (and putting aside the more complicated nesting in the logos case), if a Trait has an AssociatedType that you require to be a ConcreteType, then the where becomes:

where GenericType: Trait<AssociatedType = ConcreteType>

That's correct.

1 Like

Nice one, thank you!