Various suggestions - empty impls, empty traits, struct field traits,

Hi!

I wanted to make a few suggestions / understand if there's already an RFC for any of these:

  1. Empty impls / Empty traits

When working with marker traits, I personally think it would be nice to be able to not supply the trailing block, as Rust currently allows for structs. e.g.:

trait MarkerTrait;
impl MarkerTrait for Entity;

vs

trait MarkerTrait {}
impl MarkerTrait for Entity {}

I personally find the former more readable (and subjectively, think it looks nicer)

  1. Struct field traits

I've seen a few questions around this in the past and a mention of a proposal, but wanted to give my two cents on why this would be useful.

Since Rust traits only allow for functions rather than fields, grouping functionality can become a bit more difficult in the case where you want a struct to have a certain shape, e.g. for serialization, to later be able to use it for a specific purpose.

For instance if I wanted to set an invariant for records that can be stored in a DB where they must have an ID field and a few other fields that must exist to be able to later query said records, I could theoretically define a trait with functions that supply those fields, but that doesn't actually guarantee anything in terms of the record shape.

  1. Allow referring to associated consts in traits

This one is similar to the previous point, I think it could be useful to be able to blanket impl a trait which rather than a function, requires the implementing type to define an associated const of a certain type, to be able to implement functionality once (e.g. in a function on the same trait which uses said const) rather than implementing the same function for each struct.

Thanks!

2 Likes

Note that struct Foo; and struct Foo {} do different things! The former adds const Foo: Foo = Foo {};, but the latter doesn't.

Since there isn't a similar thing for impls and traits, I don't think it's worth having another way to write the same thing.

(I do think ; looks a bit nicer than {}, but not enough to need to support both nor to force everyone to transition over an edition.)

9 Likes

Thanks @scottmcm! I wasn't aware of the difference.

Regarding the point on an edition - not sure I understand why one would be needed, would this not be backwards compatible with existing code?

Are new editions not normally created in cases where for example a new keyword could break existing variables with the same name?

Thanks!

Regarding the const creation, was the decision to allow the short form for convenience or for another reason?

(Perhaps "unit" enum variants (with no data) use the same machinery, since enum variants behave like structs syntactically?)

Thanks!

Edit: removed part of the question since it wasn't relevant / correct

If you're curious, see https://rust-lang.github.io/rfcs/1506-adt-kinds.html#unit-structs

It would absolutely be backward compatible to support both. What I was referring to there is that I'm not a fan of both, so if we wanted to remove {} support (to no longer have two ways of writing it) then that would take an edition.

1 Like

I think it'd be nice to have. It's not super important, but I remember being slightly annoyed that unsafe impl Send for Type {} requires useless brackets, and that my editor and rustfmt have different opinion on their placement.

6 Likes

I see, although I'm not sure (maybe missed it in the link) whether struct Foo; has a const to make usage easier (in which case it's a change for convenience on top of a change for convenience, which theoretically means two different ways to do the same thing has "precedent"), or if it has another reason for the difference in which case it's not actually two ways to do the same thing.

Mainly asking out of curiosity, although personally I'm not sure I'd be against having both, there's other examples (like where) where there's multiple simultaneous options

The post itself has three (unrelated) points by the way, in case it's unclear that the latter two are not a continuation of the first

This is already possible, unless I misunderstood you.

2 Likes

You're right!

The post originally stated a blanket impl for a struct with a const which I don't think is possible but perhaps I didn't find it, but I edited the description and dropped the word impl so you're entirely correct that it exists (I also wasn't aware of this existing and it solves my original issue, but perhaps impl for a struct with const x still makes sense)

Edited the original

I would consider this an implementation detail. The const item is just added to also allow omitting the {} at the usage site. Other than that, it doesn't affect behavior in any way; when importing a unit struct, the const item is implicitly imported as well.

Note that enum variants also allow omitting the {}, which is equivalent to declaring an associated const with the same name, except that this doesn't actually work.

Personally I'm not opposed to allowing ; in addition to {} after traits and impls. The main question is where we draw the line.

  • Do we also want to allow ; after functions with an empty body? No, because that means something very different in a trait.
  • Do we want to allow it after empty enums and unions? Maybe for consistency.
  • expressions such as if, while, loop and for? Probably not, although there is precedent in other programming languages.
  • macro calls? Maybe.
2 Likes

I'm surprised about the const item detail, I thought it was a literal 'static expression of the sort &0 is. Is there an observable difference? Is it affected by type aliases?

Empty traits/impls should certainly be identical regardless of being spelled with a ; or {} - if nothing else anything else would break all the derived code out there AFAICT.

For some reason, enum Foo; (and union) makes my skin crawl - I have no idea why? Maybe because empty enums are definitely not a "default" thing?

Absolutely not for control flow: otherwise if some long confusing condition; { always runs } is possible. This has burned people in other languages.

I think it makes sense in macros, (only in item position, of course), for cases where you just need to paste the same items a bunch of places (presumably using the deliberate lack of item name hygiene, iirc?)

Regarding the OP second point, I've always wondered why this hasn't been a thing, in C++ or Rust or whatever. It seems reasonable to me that

trait Record {
  fn save(&self);

  id: u32;
}

Would define a vtable layout with two entries, the address of an fn save(), and the object-relative offset to a u32. I guess the impl looks like:

struct User(u32);

impl Record for User {
  fn save(&self) ...

  id = 0; // as in, member 0, not the value 0 - though the offset in this case would be.
}

So that's a bit weird.

That's not how traits work in Rust. Traits are compile time constructs. Only if you use a dyn Record type (which is type erased at run time) would there be a v-table.

e.g.

fn foo (record: impl Record) {
    record.save() // there is no v-table lookup here, it's a static call
    //...
}

Yeah, I was talking about the dyn case, because the impl case is obvious.

Well, the example provided is incorrect/incomplete and the phrasing confusing.

Let me try to address your point though:

  1. The trait definition syntax is shared between the two cases, dyn trait is the syntax at the usage site. It isn't obvious at all that it is the dynamic case as you claimed.
  2. A type can implement multiple traits in an open world design - this posses multiple issues: If I do impl Foo for Bar {} and Bar is defined by another crate which was compiled separately - how do we define the field mapping and how would the compiler know how to resolve this? If I combine multiple traits by e.g. defining dyn (X + Y) how would that work?

You're being needlessly pedantic. I was talking about a vtable it would define, so yes, I feel it's pretty obvious that I'm talking about the vtable it would define for when vtables are used.

You can define a field mapping for a foreign type only if the field is public. Perhaps there's some ecosystem impact here given that public fields are pretty uncommon, but you always either control the type or trait, so I don't really see what it would be.

You can only "combine" empty traits like that in current Rust, so it wouldn't be legal. If it is supported at some point, then the field offset is just another vtable entry defined by the impl, I don't see the additional problem it would cause. It would probably just be defining a new vtable concatenating the entries, for example, so what's in the entries doesn't matter at all.

That's a bit rude. I'm simply pointing out that you're changing the trait definition for the purposes of a specific usage pattern without addressing what would be the semantics of said extended definition for other usage patterns.

On the other points, combining multiple non-empty traits is a limitation today due to implementation concerns. Afaik it is definitely planned to loosen that restriction eventually. There has been some work towards that as part of working towards supporting up casting.

And lastly, the open world design means this needs to accommodate separate compilation as each crate is compiled separately.

I did not say it is impossible to achieve your goals. Merely asking to clarify how these points would be accommodated in your suggested design. Your design is simply incomplete without addressing these prerequisites.

Do you have any specific issues I haven't addressed yet? Is there any reason you feel so strongly about the change, even if it's not technical? It's not like I'm even saying it would necessarily be a good idea, just that I don't see why it couldn't or shouldn't be done.

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