Confusion around what GATs (Generic Associated Types) are

I managed to stumble into this error (Edited for formatting):

error[E0658]: where clauses on associated types are unstable
  --> src/lib.rs:6:5
6 |     type Output where Self::Output: ::std::fmt::Display;
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: for more information, see https://github.com/rust-lang/rust/issues/44265
  = help: add `#![feature(generic_associated_types)]` to the crate attributes to enable

And I think now I understand what GATs actually are (or could be).

I'm trying to make a "helper trait" to complete assignments, which in essence is to read input, parse it and Display an Output.

Without being able to specify that the Output associated type implements Display, my trait fn for display looks a bit weak, and I can't provide a default implementation, which would be something like:

fn display(output: Self::Output) {
    println!("{}", output);
}

I'm still experimenting but envisage a consumer (lazy me) using the trait like so:

struct MySolution {};

struct MyData {
    x: u32,
    y: u32,
};

impl Assignment for MySolution {
    type Input = MyData;
    type Output = u32;

    fn parse(input: String) -> Self::Input {
        let input = input.split_whitespace();
        let x = input.next().unwrap();
        let y = input.next().unwrap();
        MyData {
            x,
            y,
        }
    }

    fn solve(input: Self::Input) -> Self::Output {
        ... a solution here ...
        4
    }
}

fn main() {
    let s = MySolution;
    s.display(s.solve(s.parse(s.read()));
}

Or something along those lines.

I've previously used cargo-aoc which uses some clever macros and derives to achieve a similar problem solving framework idea but I'm apprehensive about writing "oh so complicated" macros myself (maybe I shouldn't be?).

Open to suggestions, but primarily sharing because I heard about GATs in various places when reading about rust features and was surprised to run into them myself.

A generic associated type is an associated type with genericity, like type Output<'a>. This doesn't seem GAT-related to me, though without knowing what you were trying to do it's hard to be sure.

That's not GAT. GAT is

trait Foo {
    type Output<'a>;
    fn get<'b>(&'b self) -> Self::Output<'b>;
}

Where bounds on Associated Type are behind the GATs feature gate because they are only useful with GAT as any bound that doesn't refer to a parameter of the GAT can be put on the trait itself

trait Foo
where
   Self::Output: Display,
{
    type Output;
}

Or equivalently

trait Foo {
    type Output: Display;
}
3 Likes

Ah, understanding GAT usecases still eludes me for now then.

Are bounds on associated types "minus GATs" possible to use already?
I was put off attempting it when rust warned me:

warning: the feature `generic_associated_types` is incomplete and may cause the compiler to crash

but if this a simplistic, not really GAT case maybe I shouldn't be so scared?

I misplaced my where clause when receiving the above error from rust about GATs (note the help: line appears on nightly but not stable): Edit: fixed playground :slight_smile: playground

which led to a (probably very strange) third variant:

trait Foo {
    type Output where Self::Output: Display;
}

At the moment I'm taking away from this that where clauses (for now) should belong to the trait definition (or struct definition...) and not the definition of an individual type.

But that makes the valid shorthand type Output: Display seem a little special?
I'm guessing it's syntax sugar of some sort?

Well, it is mostly a matter of ergonomics, since most of the time one can deal with the lack of GATs by moving the generic parameters from the associated function(s) to the trait containing them. But that hinders the readability of the trait, since it needs to be infected with the genericity of ALL these associated functions.

For instance, compare:

trait Trait<'a, 'b, 'c> {
    type RetA : 'a;
    type RetB : 'b;
    type RetC : 'c;

    fn a (&'a self) -> Self::RetA;
    fn b (&'b self) -> Self::RetB;
    fn c (&'c self) -> Self::RetC;
}

with

trait Trait {
    type RetA<'__>;
    type RetB<'__>;
    type RetC<'__>;

    fn a (&'_ self) -> Self::RetA<'_>;
    fn b (&'_ self) -> Self::RetB<'_>;
    fn c (&'_ self) -> Self::RetC<'_>;
}

There is also a semantic difference, but that has to deal with HRTB and late vs. early binding of some generic parameters, but thats's a quite advanced and thus niche use case.

Click here to waste precious minutes of your life

Take the following function:

fn foo<'lifetime> (_: &'lifetime ())
{}

and now imagine using it as this:

let f = foo;
{
    let local = ();
    f(&local);
}
let local2 = ();
f(&local2);

And so does the following code:

let f = |x: &_| foo(x); // generic lifetime
{
    let local = ();
    f(&local);
}
let local2 = ();
f(&local2);

But the following code fails to compile:

let f = |x: _| foo(x); // inferred thus fixed lifetime
{
    let local = ();
    f(&local);
}
let local2 = ();
f(&local2);

This is because in the second example, type inference leads to a fixed ("early bound") lifetime parameter, and the code with the locals is so that there is no single lifetime applicable to both cases. The lack of genericity leads to this failing.

Another example of an early bound lifetime is the following one:

struct Lifetime<'lifetime> ...

impl<'lifetime> Lifetime<'lifetime> {
    fn foo (_: &'lifetime ())
    {}
}

fn main ()
{
    let f = Lifetime::foo; // 'lifetime inferred, thus fixed
    {
        let local = ();
        f(&local);
    }
    let local2 = ();
    f(&local2);
}

So, back to the topic of GATs, the
trait Trait<'a> { type Ret : 'a; ... }}
has the disadvantage of suffering from the early bound lifetime pattern from the previous example:

trait Trait<'a> {
    type Ret : 'a;
    
    fn foo (&'a self) -> Self::Ret;
}

fn test<T : Clone> (x: T)
where
    T : for<'any> Trait<'any>,
{
    let foo = T::foo;
    {
        let local = x.clone();
        foo(&local);
    }
    let local2 = x;
    foo(&local2);
}

Whereas

#![feature(generic_associated_types)]

trait Trait {
    type Ret<'__>;
    
    fn foo (&'_ self) -> Self::Ret<'_>;
}

fn test<T : Clone> (x: T)
where
    T : Trait,
{
    let foo = T::foo;
    {
        let local = x.clone();
        foo(&local);
    }
    let local2 = x;
    foo(&local2);
}
5 Likes

Yes but the trait will then be

trait Trait<'a, 'b, 'c> 

instead of

trait Trait

It might become a problem when later somebody tries to concrete the trait.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.