Changing the `impl Trait for .. { }` syntax

So I want to prepare an amendment to (of replacement of) the auto trait RFC which clarifies a bunch of things and brings it more in line with the current implementation. (Among other things, formalizing the name auto trait in place of OIBIT.)

One thing that I am not sure about: I do not like the impl Trait for .. { } notation. I don’t like it because it’s wacky and special-purpose; also, the fact that an auto trait is an auto trait is very important and seems like it should be declared upfront.

EDIT: I’ve been persuaded towards auto trait Foo { } as the syntax instead of my (slightly different) original proposal.

Original text:

What do people think about: auto trait Foo; as an alternative syntax? auto would be a contextual keyword here. If it is an unsafe trait, it would be unsafe auto trait Send.

Note the use of ; – auto traits cannot have items. I would also be fine with auto trait Foo { }.

Clarification: note that the current syntax is unstable, of course. :slight_smile:

5 Likes

I don’t like a new contextual keyword. I think any new contextual keywords should be added with caution. I also don’t really like auto trait Foo; too many keywords in a row.

Here are some okay alternatives:

trait .. Foo;
trait Foo..;
1 Like

I also considered trait Foo for .., but I think I prefer auto trait. I like that it tells you the name of this feature vs begin some kind of obscure sigils. I do agree that unsafe auto trait Foo is a lot of keywords in a row, but then…it’s not something you declare very often.

2 Likes

It’s probably a stupid idea, but what about a builtin supertrait that makes any inheriting trait automatic:

#[lang = "auto-trait"]
pub trait Auto {}

pub trait Foo: Auto {}

// Default impl is implied, but also obvious in the trait declaration
// impl Foo for .. {}

impl !Foo for Bar {}

This doesn’t introduce any new syntax or keywords, and it’s immediately intuitive. Additionally, the documentation for the Auto trait can describe its behavior, trivially connecting the abstract to the implementation.

7 Likes

Intriguing. I think I like it.

1 Like

Presumably only traits that immediately extend Auto would be auto traits. For example, in the following, only Send is actually an auto trait:

#[lang = "auto_trait"]
pub trait Auto {}

pub trait Send: Auto {}

pub trait Foo: Send { ... }

Likewise, it seems confusing that this would mean that for any type that implements an auto trait, the type would also implement Auto.

A variant of this could be:

#[lang = "auto_trait"]
pub trait Auto {}

pub trait Send {}

impl Auto for Send {}
4 Likes

I think both a keyword on the trait decl and a supertrait are non-ideal because they would prevent Rust from ever having traits which are automatically implemented with a negative polarity. Even though this feature is not obviously useful, the fact that this feature makes sense I think should inform us that the “auto trait” property is a property of an impl, not of the trait itself.

Therefore I would propose something more along the lines of auto impl Send; or auto impl Sync (and, hypothetically, auto impl !NotSend; and so on).

I also think just swapping .. for _ would be a clear improvement - impl Send for _; but it doesn’t fully capture the nuanced semantics of auto traits.

Inheriting Auto from another trait would probably be breaking, yeah. I’ve seen many traits in the wild that inherit from Send that wouldn’t work as auto-traits, usually because they have items. Although, if a trait inherits from an auto-trait and doesn’t have items, then it’s probably going to be an auto-trait itself. It seems like the intent to implement Auto could be deduced from the presence or absence of items.

I’m not of a fan of impl Auto for Foo {}, partially because it separates the auto-trait marker from the trait declaration. It also looks like an extension of impl Foo which is used to declare items on trait objects.

prevent Rust from ever having traits which are automatically implemented with a negative polarity

What would be the functional difference between an auto-negative trait and a trait which just doesn't have a default impl?

Its connected to a new feature which would allow T: !Trait bounds. The tl;dr is that type/trait relations are broken into three buckets instead of two - right now its "implements" or "does not implement;" instead its "implements," "does not implement," and "may implement." The key distinction is that by default types are in the "may implement" bucket, and you get into the "does not implement" bucket by providing an explicit impl !Trait for Type item, essentially guaranteeing that you won't ever implement this trait in (minor) version upgrades. This avoids some bad backward compatibility hazards with negative bounds.

So auto impl !Trait; has the same semantics as auto impl Trait; except that it puts types into the second bucket by default.

Hmm... it might be worth considering whether Rust will ever support traits that are implemented on higher-kinded types, and if so, what the syntax might look like. I say this because Auto is more like a kind, in the sense of being part of the 'type' of the trait (even if this isn't a real language concept now or ever) than a supertrait (what does it mean for T: Auto?), while 'traits that are implemented on higher-kinded types' could be viewed as 'higher-kinded traits', so it might be possible to use the same syntax for both. I don't really know whether this would make sense... it probably wouldn't, since the syntaxes that come to mind for me don't have a natural way for auto traits to fit in. But I guess I'll mention it anyway.

I really dislike auto trait T. The trait itself is not really special, it’s the implementation of the trait that is special. It should rather be auto impl than auto trait.

The alternative trait T: Auto is even worse IMO. Why should adding a parent trait change the impls of the dependent trait? This is completely unexpeced.

Both proposals add unnecessary inconsistencies to the language for little gain.

Rather than adding more and more special cases, there should be a possibility to denote all types that contain other types in a generic manner, e.g:

struct<A, B>

could be a builtin trait to denote all types that contain only fields of type A and B.

Then extend the syntax to something like:

struct<..: Send>

to denote all types that contain only fields that implement Send

impl<T : struct<..: Send>> Send for T;

Sure, this is more complicated than just auto trait and it probably also needs some kind of HKT and a bit of new syntax, but at least it is not completely surprising.

Yeah, after I wrote my comment, as I went to bed, I got to thinking that abusing the supertrait relationship in this way was not such a good idea, and these implications are a good example of why.

Put another way, trait Foo: Bar means "every type that implements Foo implements Bar", and (ab)using a special Auto trait to mean something about Foo just has nothing to do with that relationship. That seems likely to lead to contortions and confusion. :slight_smile:

Too bad, it was a cute idea.

1 Like

This is not true. The trait is special. For example, auto traits use a coinductive matching strategy, unlike normal traits, which are inductive. As a consequence, auto traits cannot have members nor can they have supertraits.

1 Like

Me not being a PL theorist, could you explain in a few words what that means? I mean in practice, how they behave differently.

Sorry, let me define this. The "coinductive" strategy means that, in short, if you have a cycle, that's ok. In other words, roughly speaking, to prove that Foo<T>: Send, you get to assume that Foo<T>: Send holds. This is required because of recursive types and so forth. (I plan to try and write-up a blog post on this going into a bit more depth; I'd love to make auto traits less special, I just don't think it works -- but in any case they will always be somewhat special.)

Heh, yes, just tried. :slight_smile:

Here is a concrete example of how they are different. If you had

struct Foo<T> {
    b: Option<Box<Foo<T>>>
}

you can imagine that the “auto trait” impl is something like

unsafe impl Send for Foo<T>
    where Option<Box<Foo<T>>>: Send
{ }

so if want to show that Foo<T>: Send, we have to first show that Option<Box<Foo<T>>>: Send. But that in turn means we must show Box<Foo<T>>: Send (because that’s what the Option impl will require) and then Foo<T>: Send (because that’s what the Box impl will require).

You might think that we could write the Send impl like so:

unsafe impl Send for Foo<T>
    where T: Send
{ }

except that this doesn’t work for all structs, most notably around associated types:

struct Bar<T: Iterator> {
    x: Option<Box<Bar<T::Item>>>
}

Here the impl would be:

unsafe impl<T: Iterator> Send for Bar<T>
    where T::Item: Send
{ }

but what if we have Bar<Baz> where Baz: Iterator<Item=Bar<Baz>>? Now to prove that Bar<Baz>: Send, we have to show that Baz::Item: Send, and Baz::Item is (again) Bar<Baz>. So we’re stuck.

(With normal impls, you would get an overflow error. With an auto trait, we consider this OK.)

Ok I think I’ve understood. But I still think that this is a property of the impl, not of the trait itself. It seems to me that the impl is co-inductive, not the trait. And I don’t understand why being co-inductive precludes having supertraits and members. Why can’t every impl (or trait if you like) be co-inductive?

To put it differently, IMO the trait itself consists only of the operation/items and the semantic meaning that it implies. Everything else is part of the impl.