Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: Implementable trait aliases #3437

Open
wants to merge 20 commits into
base: master
Choose a base branch
from

Conversation

Jules-Bertholet
Copy link
Contributor

@Jules-Bertholet Jules-Bertholet commented May 24, 2023

Rendered

Allow writing impl blocks for certain trait aliases.

Prior discussion on Internals

@rustbot label A-traits

@rustbot rustbot added the A-traits Trait system related proposals & ideas label May 24, 2023
@ehuss ehuss added the T-lang Relevant to the language team, which will review and decide on the RFC. label May 25, 2023
- Better ergonomics compared to purely proc-macro based solutions.
- One alternative is to allow marker traits or auto traits to appear in `+` bounds of implementable aliases.
(For example, `trait Foo = Bar + Send;` could be made implementable). However, I suspect that the complexity would not be worthwhile.
- Another possibility is to require an attribute on implmenentable aliase; e.g. `#[implementable] trait Foo = ...`. Again, I don't think that the complexity is warranted.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one benefit of requiring #[implementable] is that it mitigates the confusing aspect of:

#[implementable] // error: must only have one trait on rhs of equal -- generated because of #[implementable]
pub trait Foo = Bar + Baz;
// vs.
#[implementable]
pub trait Foo = Bar
where
    Self: Baz;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added some more discussion about this.

@burdges
Copy link

burdges commented May 25, 2023

As an aside, it'd be nice if type aliases permitted instantiation:

pub struct FooT<T>([T; 32]);
pub type Bar = Foo<u8>;

impl Default for Bar {
    fn default() -> Bar {
        Bar( Default::default() )   // Forbidden right now.
    }
} 

@programmerjake
Copy link
Member

programmerjake commented May 25, 2023

As an aside, it'd be nice if type aliases permitted instantiation:

they do, if you use curly braces (except for tuples):
https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=b95fe39281037ac567cfc8dfa7aba44b

i think it'd also be nice to allow curly brace syntax for tuples for ease of writing proc macros so there's one consistent syntax that works on all struct-like types:

type T<A, B> = (A, B);
let v = T::<_, _> { 0: "abc", 1: 123 };
match v {
    T::<_, _> { 0: a, 1: b } => println!("it works! a={a} b={b}"),
}

@dlight
Copy link

dlight commented May 26, 2023

they do, if you use curly braces (except for tuples):
https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=b95fe39281037ac567cfc8dfa7aba44b

Wait, so T { 0: 5 } works but T(5) doesn't? This sounds like a bug.

@Jules-Bertholet
Copy link
Contributor Author

Jules-Bertholet commented May 26, 2023

Wait, so T { 0: 5 } works but T(5) doesn't? This sounds like a bug.

Changing this behavior would be a breaking change, for code like the following:

struct Foo();
type Bar = Foo;
fn Bar() {}

Anyway, this is all a bit off-topic for the RFC 😁

@nielsle
Copy link

nielsle commented May 28, 2023

I think that the RFC should mention #1672 and mutually exclusive traits

rust-lang/rust#20400
https://geo-ant.github.io/blog/2021/mutually-exclusive-traits-rust/
https://stackoverflow.com/questions/57749827/mutually-exclusive-traits

Is this RFC an alternative to #1672 or is it orthogonal?

@Jules-Bertholet
Copy link
Contributor Author

@nielsle I don't see any relationship between this RFC and that issue.

@tmandry
Copy link
Member

tmandry commented Sep 7, 2023

I think the summary motivation section should be fleshed out more, ideally with real-world examples (possibly simplified). After looking through the text this does seem useful but it wasn't obvious at all to me what the feature was from those sections.

@Jules-Bertholet
Copy link
Contributor Author

I've specified that implementable trait aliases also support fully-qualified method call syntax.

@nikomatsakis
Copy link
Contributor

@rustbot +I-lang-nominates +I-types-nominated

I'm nominating this for @rust-lang/lang and @rust-lang/types discussion. I myself am in favor of this RFC. I've seen a lot of demand for a simplified version of trait aliases where--

(A) We only support trait Foo = Bar + Baz format, essentially.
(B) The trait Foo is implementable and, in general, acts like a "normal trait" so that users don't have to know about the more detailed version.

I expect this to be relevant to async fn in traits as well (cc @tmandry) it's quite common to have something like trait Service (which requires Send) and trait LocalService (which does not).

Restricting to point (A) has the advantage of avoiding some complex corner cases. Note though that I do want to support the "inline bound" syntax, so that you could do trait SendIterator = Iterator<Item: Send> (as well as RTN if we adopt that), so that trait aliases can put bounds on associated types.

@nikomatsakis
Copy link
Contributor

I guess that my question to the lang/types teams, respectively, are:

  1. Lang: what do people think about the motivation here as well as the more limited proposal? Are there important trait alias use cases not covered there?
  2. Types: are there concerns about the limited proposal that I put forward?

@nikomatsakis
Copy link
Contributor

One though -- @Jules-Bertholet -- I'm not sure if the RFC covers it, I didn't have time to read in super detail, but I think that if you are implementing trait Foo = Bar + Baz, that implies "unioning" the methods from Bar and Baz into one impl block. Does the RFC discuss this?

I have, for a long time, wanted the ability to implement a trait and its supertraits together in a single impl block, I wonder if it's worth thinking about that as well, though I'd probably want to separate it out from this RFC.

@fmease fmease added I-lang-nominated Indicates that an issue has been nominated for prioritizing at the next lang team meeting. I-types-nominated Indicates that an issue has been nominated for prioritizing at the next types team meeting. labels Sep 16, 2023
@Jules-Bertholet
Copy link
Contributor Author

One though -- @Jules-Bertholet -- I'm not sure if the RFC covers it, I didn't have time to read in super detail, but I think that if you are implementing trait Foo = Bar + Baz, that implies "unioning" the methods from Bar and Baz into one impl block. Does the RFC discuss this?

The RFC does not do any sort of trait unioning like this, I hadn't even considered it as a possibility (I will add it to the alternatives section). Notably, you would need to handle name collisions.

@Jules-Bertholet
Copy link
Contributor Author

I've added text to the alternatives section addressing @nikomatsakis's "unioning" idea.

@nikomatsakis
Copy link
Contributor

@Jules-Bertholet

Notably, you would need to handle name collisions.

Yes. Presumably that would be an error.

@Jules-Bertholet
Copy link
Contributor Author

Presumably that would be an error.

That would have backward-compatibility implications, if one of the traits adds a defaulted method that conflicts. Technically "minor" breakage, but still not ideal.

@traviscross
Copy link
Contributor

@rustbot labels -I-lang-nominated

This was discussed in the T-lang design meeting today. People do seem to want this in some form, but the RFC is likely going to need some iteration. Let's remove the nomination while we give time for this iteration to be done. Please renominate when you think T-lang should look back into this.

@rustbot rustbot removed the I-lang-nominated Indicates that an issue has been nominated for prioritizing at the next lang team meeting. label Oct 25, 2023
@Jules-Bertholet
Copy link
Contributor Author

I've made some changes in response to the design meeting (Zulip thread, HackMD) held earlier today. Notably, I've:

  • Clarified the language around how and where where bounds are enforced.
    • The new rules bring this RFC more or less in line with @nikomatsakis's model of "desugaring to a blanket impl."
  • Removed the restriction that implementable trait aliases must use all generic parameters as parameters of their primary trait.
  • Added a few more sentences on why the syntax is the way it is.

One thing I haven't yet updated is the motivation section. Many people have pointed out that they would like motivating examples that don't rely on unstable features. I plan to get to that Soon™—but in the meantime, if anyone has a good real-life example, feel free to share.

Also in the design meeting, many people had thoughts on this RFCs relationship to LendingIterator/GATification. Because that part of the RFC is so speculative, I don't think it would be wise to expand on it here, but perhaps we should discuss this in another forum (Zulip or Internals?).

@nikomatsakis
Copy link
Contributor

I found the motivation in the RFC a bit off-target. I think that the 'weak-strong' concept is correct, but not really specific to trait aliases. Every subtrait corresponds to a "refined" view of the supertrait (sometimes with extra capabilities/methods as a result of that refinement). I think of trait aliases as really just a shorthand for declaring refined traits in cases where no "per-type behavior" is needed (i.e., because there are no new methods, or even (eventually) because the definition of the methods is always the same).

To my mind, the real motivation here is expanding on the idea of trait aliases as being able to provide a "complete abstraction", where users don't have to know about the "contents" of the alias, they can treat it "as if" it were a standalone trait. I think the RFC started in the right vein there -- trait aliases are used to provide a "simplified, convenient view" onto a more complex underlying reality, but they're not able to truly hide that from users.

Eventually I would like it if users could do the same for any trait -- i.e., if I have a trait Foo: Bar, I should be able to implement Foo and Bar together etc. But I don't think that has to be in this RFC, and it'll take a bit of thought to figure out how it should work best. Still, the intution makes sense to me -- i.e., if I am in some OOP language, I don't have to separately define Shape and Rectangle and Square, I can just define Square and that Rectangle/Shape come "for free" with that.

@Jules-Bertholet
Copy link
Contributor Author

I think of trait aliases as really just a shorthand for declaring refined traits in cases where no "per-type behavior" is needed (i.e., because there are no new methods, or even (eventually) because the definition of the methods is always the same.

I agree with this framing, that's a good way of putting it.

@Jules-Bertholet
Copy link
Contributor Author

I've added a subsection to the future possibilities that explores allowing trait aliases to define their own associated types and consts, and the additional backward-compatibility benefits that would bring.

@Jules-Bertholet
Copy link
Contributor Author

Having given it some more thought, I now suspect that associated types in trait aliases could be sufficient to GATify Iterator without a separate "variance bounds" feature:

pub trait LendingIterator {
    type LentItem<'a>
    where
        Self: 'a;

    fn next<'a>(&'a mut self) -> Option<Self::LentItem<'a>>;
}

pub trait Iterator = LendingIterator
where
    // Still need to resolve implied `'static` bound problem
    // (https://blog.rust-lang.org/2022/10/28/gats-stabilization.html#implied-static-requirement-from-higher-ranked-trait-bounds)
    for<'a> Self::LentItem<'a> = Self::Item,
{
    type Item;
}

@Jules-Bertholet
Copy link
Contributor Author

associated types in trait aliases could be sufficient to GATify Iterator without a separate "variance bounds" feature

I've added a section to the future possibilities discussing this.

@Jules-Bertholet
Copy link
Contributor Author

I've rewritten the AFIT example in the motivation section in terms of the trait-variant crate.

@Jules-Bertholet
Copy link
Contributor Author

There's one issue I hadn't noticed before that limits the applicability of this feature to a degree. Currently, if you have:

pub trait Foo { /* ... */ }
pub trait SendFoo: Foo + Send {}
impl<T: Foo + Send + ?Sized> SendFoo for Foo {}

Then, dyn Foo + Send and dyn SendFoo are distinct types. Migrating to:

pub trait Foo { /* ... */ }
pub trait SendFoo = Foo where Self: Send;

Makes these into the same type, which is a breaking change (trait impls could start overlapping). This isn't a problem for the async Send variant use-case, as traits with async methods aren't (yet) object safe, but it could potentially make migrating from blanket impls to implementable trait aliases a breaking change in some other cases.

@traviscross
Copy link
Contributor

@rustbot labels +I-lang-nominated

We discussed this in the lang planning meeting today, and it looks like there have been updates since we last looked at this, so let's renominate so we can discuss.

@rustbot rustbot added the I-lang-nominated Indicates that an issue has been nominated for prioritizing at the next lang team meeting. label Jun 12, 2024
@willmtemple
Copy link

willmtemple commented Jun 23, 2024

@Jules-Bertholet I have another use case that I haven't seen mentioned here: I want to be able to hide a trait behind a module using a proc macro. I want to do this to create a sort of RTTI system for tracking which types implement traits dynamically (along with constructing other forms of metadata about the implementation). This is all in service of building a scripting language on top of Rust.

Here's a really simplified example:

#[runtime_trait]
pub trait Foo {
  fn bar(&self) -> i32;
}

would be rewritten to

#[doc(hidden)]
trait __Foo {
  fn bar(&self) -> i32;
}

pub mod Foo {
  pub trait Foo = super::__Foo;
  
  #[::linkme::distributed_slice]
  pub static IMPLEMENTORS: [TypeId];
}

And the impl:

#[runtime_trait_impl]
impl Foo for T {
  fn bar(&self) -> i32 { 3 }
}

would be rewritten to:

// Relies on being able to impl __Foo through the trait alias inside the module.
impl Foo::Foo for T {
  fn bar(&self) -> i32 { 3 }
}

#[::linkme::distributed_slice(Foo::IMPLEMENTORS)]
static __FOO_T: TypeId = TypeId::of<T>();
  • The trait itself cannot be declared inside the Foo module, since the module does not inherit scope from the outer module, and that will break the written definition of the trait's constraints and items.
  • If I give the module a different name from the trait, the user has to import an "invisible" module generated by the proc macro anywhere they want to use #[protocol_impl], and that would suck.
  • I've removed a lot of complexity around handling generic traits, but the general idea could hold for generic traits as well.

TL;DR I think this would be really useful for syntactic manipulations with proc macros and would give macro authors more flexibility in rewriting traits.

@Kixunil
Copy link

Kixunil commented Jun 27, 2024

I have a real-world example as well. My push_decode crate provides an abstraction over both sync/async and std/no_std encoding. This is achieved simply by requiring the implementors to provide an encoder - a state machine that implements the encoding. I'd love to also provide traits that take care of encodable objects. However there are various strategies used to encode things. e.g. u32 can be encoded as littel endian, big endian, varint... So what I want is this:

// Analogous to serde::Serialize
pub trait Encode<Strategy> {
    type Encoder: Encoder;
    // additional things like const MIN_SERIALIZED_LEN: usize
    fn encode(&self) -> Self::Encoder;
    // various helper methods taking advantage of the additional items. E.g. fn encode_to_vec(&self) which reserves MIN_SERIALIZED_LEN
}

Then each protocol can define enum ProtocolName {} and impl Encode<ProtocolName>. However crates with many encodable types will have to write Encode<ProtocolName> everywhere and import ProtocolName. This is annoying and it'd be much nicer to define an alias trait Encode = push_bytes::Encode<ProtocolName> and then just use Encode everywhere.

@traviscross
Copy link
Contributor

@Kixunil: Thanks for the report of the real-world example. That pattern is called "tagged impls". It provides one way to work around the current orphan rules. E.g.:

Playground link

Implementable trait aliases do indeed make this pattern more convenient.

Comment on lines +465 to +467
- One alternative is to allow marker traits or auto traits to appear in `+`
bounds of implementable aliases. (For example, `trait Foo = Bar + Send;` could
be made implementable).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm in favor of this, because while the symmetry with impl blocks is nice I think the symmetry with bounds is ultimately more important (and more confusing to break). If other lang team members agree I think we should go ahead and support it on day 1.

Otherwise, given that this is something we can allow in the future, can it be listed as a future possibility?

@nikomatsakis
Copy link
Contributor

nikomatsakis commented Jul 3, 2024

We discussed this RFC in our triage meeting today.

The meeting consensus was that we'd ideally like to have an RFC that lays out a forward looking plan that goes a bit further than the specifics in this RFC:

  • It should be possible to implement a trait and its supertraits together
  • trait Alias<T..> = Bound.. where WC... should be equivalent to the pair of
    • trait Alias<T..>: Bound.. where WC... {}
    • impl<Self: Bound.., T..> Alias<T..> for Self where WC...
    • If there are no where clauses, this is true today:
      • forall T. T: Alias => T: Bound.. because of the supertraits
      • forall T. T: Bound.. => T: Alias because of the impl
    • I don't think THIS RFC should specify the behavior for WC, only that we want the equivalence to hold. The correct behavior of where-clauses for trait definitions (in particular the implied bounds expansion) can be covered separately.
  • Combining these two, impl Alias for Type should work and be equivalent to impl Bound0 for Type ... impl BoundN for Type.

Together (I think) these changes also allow for other transformations. It'd be good to capture these as part of the RFC's motivation. In addition to the "trait alias" = "trait + impl" equivalence above, the other equivalence we had in mind was being able to convert trait Foo { fn a(); fn b(); } into trait Foo: Bar { fn a(); } trait Bar { fn b(); } without breaking existing impls.

Note that just because the RFC includes this content doesn't mean we have to stabilize or even implement all of it at once. I think it'd be useful to identify "tiers" of support. In particular

Trait aliases that include bounds with one primary trait + auto or marker traits but no where-clauses

seems like an important tier that more-or-less works today.

@nikomatsakis
Copy link
Contributor

nikomatsakis commented Jul 3, 2024

This wasn't discussed in the meeting, but I personally would go further and say that my ideal is that you can do impl $Bound for Ty for any bound (i.e., anything that could come after a :). That would mean impl Debug + Display for Type etc. Basically, if you can do trait Printable: Debug + Display and then impl Printable for Type and have that cover all 3, why not be able to say it explicitly? (That example is maybe not so great because both traits offer a fmt method, so it may not be covered by the proposal without adding some way to disambiguate...)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-traits Trait system related proposals & ideas I-lang-nominated Indicates that an issue has been nominated for prioritizing at the next lang team meeting. I-types-nominated Indicates that an issue has been nominated for prioritizing at the next types team meeting. T-lang Relevant to the language team, which will review and decide on the RFC.
Projects
None yet
Development

Successfully merging this pull request may close these issues.