Trait implementation variables - "impl vars"

Hey all,
   I had a lightbulb moment yesterday (I believe) while on the toilet.
   Anyways, the idea is simple.
   If a trait is being implemented, allow the "struct level" variables to be defined inside the "impl" block (above the method implementations). This would be a "guise" of sorts for a struct variable (that the compiler merges into the struct at compile time). That way, all the methods for that implementation can share the same instance level data, but keep out of the hands of other methods implemented on the struct (whether or not for some other trait).
   Benefits include:

  1. Enhanced cohesion
  2. Enhanced "protection" of struct variables related to a trait impl.
  3. Better ergonomics - (rls drop down lists will only show that variable if you are in a method that is in the impl block of the trait impl the variable is defined in).
  4. A road to trait implementation based "mixins" with instance data.

   I believe this change could also be propogated to other languages eventually (java, C#, typescript, etc) - they need to add a "block" for the implementations first, then can define there. The note about mixins is basically, we could impl the trait with a special modifier that indicates it's a mixin, and that the mixin's "instance fields" will get merged into the struct (any struct) - so not "impl for struct", but "impl for mixin" or something, then a way to mix that in ... something like that.
   I personally am seeing something huge here from a standpoint of code separation, but I could be missing something.
   Let me know if this makes sense, or if I should have just stayed on the toilet.

Jeff

Can you provide one or more example code that uses this feature, and describe what each example does?

1 Like

I'm laying out the simplest method I can think of - just one variable it sets. The variable, my_trait_related_var would only be settable and retrievable within methods in this particular impl block. It's another "layer of privacy", but not only is it private to the module, but also hidden from rest of the struct's implementations, fully shareable across all methods in the same "impl block". You can of course also work with the structs regular fields themselves for non-private stuff. Basically, it's part of "self" - just tucked away from other's eyes (that aren't part of this particular impl)

impl MyTrait for SomeStruct {
     let my_trait_related_var = 50;

     fn my_trait_method(&self){
         self.my_trait_related_var = 80;
    }
}

Doesn't this mean you can no longer know the fields of a type just by seeing its definition?

What about types defined in other crates? Are you allowed to add fields to those types? What if those types are #[repr(transparent)]/#[repr(C)] and someone relies on that?

What about enums? Is it allowed? How is it treated?

I see you declared it with let and without specifying a type. How would type inference work for that since functions are type checked separately?

9 Likes

They would be "private to the trait implementation" essentially, so you wouldn't really care about them unless working on the trait, in which case, they're right there. They'll still show up in RLS drop down lists IF you're in one of the methods in that impl. Other methods on the struct will not see in their RLS drop down lists. Much less confusion on similarly named or misnamed variables (that get used by some other method outside of that trait impl that shouldn't be touching them). The struct attributes would still be able to be added to them. Basically, they're just "moved down" and out of reach, and can be "hoisted back up" to the struct when compiled.

There's an unaddresses fundamental question, how is a struct with these ad-hoc fields constructed? Do they just get assigned the value written in the impl block?

Also, this seems like a problematic proposal for types that have a guaranteed layout, seems to me that it should be opted-in at the type declaration.

Third, this interacts badly with std::mem::size_of, since the size of a type depends on downstream crates.

5 Likes

Would this allow adding a field to any (not specially marked) struct from outside of the crate in which is defined? If so:

  1. That makes it impossible for library authors to properly optimize for enum sizes well since you can no longer predict the sizes of any structs you use in one or more variants.
  2. Auto-traits would become unpredictable, e.g., if a non-Copy field is added to a struct.

Furthermore, I imagine this would not be allowed for generic/blanket implementations? Cause otherwise the following:

impl <T> MyTrait for T {
     let my_trait_related_var = Rc::new(());
}

would make everything non-Copy and non-Send.

Instead of designing in the abstract, could you possibly supply a semi-real problem you could solve with this feature with a concrete example? That would help determine how one could impose constraints on the functionality to still be useful.

3 Likes

I wasn't envisioning as such. I was more envisioning for your own internal implementations. Generics would be allowed as I see it - the "field" is shared "within the impl" only. It's private, so if it needs to be renamed by compiler to make it unique, that's okay.

This one is VERY abstract. It's all principle of the matter. Keep your chocolate out of my peanut butter. I'm not saying there's not stuff to think about - I'm certainly not a language author and only looking for "app dev" perspective - how to separate the code to keep it easy to understand and maintain. The example I put above is only one I'll be adding. It shows the core of the usage.

That said, it may or may not be possible with Rust. Lots to think about likely, but worth the venture too if it works out. It would be HUGE... especially for "enterprisy" type applications that have objects with numerous aspects to them.

Well, I've think you've noticed some issue with the way I wrote the sample. It's just a very high level conceptual 'idea' at this point, and the details of how things are defined specifically for the solution would be worked out during the implementation (if it happens), but the idea is simply to move it into that block to protect it further (from the hands of other methods implemented on the struct outside of this impl block). Then a dev doesn't have to ask, "Is this the variable I should be using"?

You didn't address my questions though:

  • there are many situations where someone cares about the fields, for example when we want them to be cheaply copiable/clonable, or we want the size of a struct to not go over a certain size.

  • you said nothing about types in other crates. If I add a field of type Vec<Foo> to NonZeroI32 I doubt it will be fine, especially if that someone is transmuting i32 to Option<NonZeroI32> and suddently the size doesn't match up anymore.

  • again, you said nothing about enums. I guess they just aren't supported, although this feels weird because this has nothing to do with impl blocks.

  • nothing about inference either. This is an implementation concern.

    impl Trait for SomeStruct {
        let foo = 1;
        fn bar() {
            let _: u8 = foo;
        }
        fn baz() {
            let _: u32 = foo;
        }
    }
    

    How is this type checked? Either foo is inferred to have type i32 just by its declaration or you need to type check both bar and baz at the same time (which AFAIK is something we don't want to do) to find out whether they agree on foo's type.

How would that even work when macros work at the token tree level? The two definitions are very far apart, there's no way a macro will be able to see both of them.

To be honest, I'm mostly here to drop off high level conceptual idea, and let the language authors work out the details. I know there are many "gotchas" and workarounds in Rust due to complexity of the language. That said, we aren't really changing things, just "hiding" them further. Keep your chocolate out of my peanut butter is the concept. My private fields in this impl block should only be touched by methods in the same impl block. It's a compiler check as I see it. I'm not saying 'easy', or no gotchas along the way. But I think very do-able.

Sorry, that's not how things work. Quoting myself from a previous request with a similarly abstract request:

If you just want to showcase something, then be clear that that is what you are doing. Don't put something on the table and say "something should be done" and refuse to explain when folks ask you what to do with your proposal.

Here, there are questions about the implications of your proposal. Now, if you want to say "do what you need, I just want my feature" and the implications that come from it and the solutions that actually end up happening make what you actually wanted impossible, you're going to be disappointed and Rust is going to have more complexity with little compensation beyond "somebody asked for something and we did it".

That's why proposals of any significant complexity (of which this certainly is) should really be done in RFC form (even if labeled as a pre- or pre-pre- stage). Its structure helps to get yourself to ask these kinds of questions before it gets wallowed in the details of how things actually end up having to work when put into instructions for a machine to actually perform.

14 Likes

The main problem is that Rust is a low-level language that cares quite a bit about such details as struct size and layout. Extra struct fields could be more easily abstracted away in a higher-level language that uses boxed representations like Java, but Rust is more of a "what you see is what you get" language.

1 Like

I won't talk about the language implementation myself - i'm not a language developer, so any words I have with regards to implementation would be meaningless, but I think those issues you mention with size and layout are things that would be part of the solution - merge into struct in a way that guarantees some layout order if needed (attribute maybe). But, I won't speak to possibility, only something desired.

But most of the objections to what you're asking for aren't about the implementation, they're unaddressed implications of the design that make it an implausible language feature. It's not like you proposed to add another sorting method, and just let the implementors do their thing.

You should decide if this is a serious proposal, or if it's just an element in some wishlist, expecting other people to design your desired feature (along with addressing problems with it) is unreasonable.

8 Likes

I'm not a language designer. I really don't know what is and isn't plausible. As a non-language designer, I certainly would not be able to even know what all the implications are, however, as an application developer, I don't see issues - we're just changing scope - hiding something.

Most people are being kind here, trying to sort out the implications and issues your proposal might introduce. Please do not shove that off easy and says "not my work, I see nothing wrong". People are exactly telling you where it is wrong.

You don't see it doesn't mean it does not exist, it's just you are not knowledgeable enough to see the problem.

9 Likes

It's like you said "I wish my car had wings!"

People are responding with practical, user-facing concerns. Where would you keep a car with wings? Is there a place near your house that would serve as a runway so you could take off? Are there runway-ready strips of road near the places you want to go? Maybe it would have to be VTOL. How far does a winged car have to fly without refueling to make it more convenient than driving? For that matter, how much cargo can it carry?

And you're saying, "I don't know, I'm not an engineer. I just think it would be cool if my car had wings."

The questions above aren't implementation details, they're essential practical concerns. A car with wings might be cool in theory, but if it doesn't fit in your garage or can't go places you want to be, it'll be useless to you. You're an ideas person, fine; people are only asking you to clarify your idea, not implement it.

"I think it would be cool if impls could have fields" isn't enough of an idea to have a meaningful discussion about. If nobody else sees your vision, that doesn't mean it's a bad idea, but it does mean you have to show people its merits. After all, if the advantages of impl fields were obvious to everyone, somebody probably would have proposed it already, right? Ideas are a dime a dozen. What makes this a good idea and not just an idea?

The best way of exploring that would be to show an example of some code in Rust today, without impl fields, and show that it has some kind of problem that would be solved by putting a field in an impl.

12 Likes

Sorry if my words came across that way - wasn't intended. I'm just not a language designer. It's just a concept at this point. I don't know how much further I can push the concept myself, as I'm not a language designer, and they have to think about a whole other domain than what I know. There would be plenty of implementation challenges - for certain. It is just me dropping off an idea. I'm still learning Rust.

I am an engineer. I'm not a language designer.

1 Like