Trait implementation variables - "impl vars"

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.

4 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.

13 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

That's fine, but you'll have to accept that without more details as to what you actually want, this is not happening at all.

Indeed. However, any progress on their part is blocked on questions about what you expect to happen in certain cases. If you cannot explain at least what might happen (not even "should") when your proposed idea is used that satisfies your own goals, I don't know how you can expect anyone else to answer them for you. Again, if this is all you do (and somehow this does get implemented), it'd be frustrating to see "but that's not what I had in mind" if there's some constraint that has to be applied during the implementation.

And yet you can't provide an example of what it looks like before and after and explain the difference? The questions here are not looking for you to answer absolutely everything, but just what you, as the idea originator, are the best person to answer.

All that said, there are a lot of effects here that this idea ends up creating that should really be hashed out up front. Of note (some of which have been asked here):

  • Does adding an "impl var" via impl Trait for T add a field to every type?
  • Can a crate change another crate's type by implementing a trait and adding a field?
  • How does this affect type layout?
  • What the heck happens with an enum?
  • Can I add fields to a ZST? Is it a ZST anymore? What happens to other crates that I depend on (and therefore cannot know about the "impl var") that assume it is (was?) a ZST?

Basically, the core question is:

  • Given a type, where does its "impl var" variables live?

If this cannot be answered, this idea is just not implementable. Of course, there are multiple possible answers for this, but they all have tradeoffs. Some off the top of my head:

Modifies the members of the struct

Pros

  • Simple in concept.

Cons

  • Does not work for types not in the crate (i.e., all top-level types in the impl line must be crate-local and non-generic).
    • Otherwise building becomes undecidable as you need to tell already-compiled crates "oh, this type has additional shadow members you need to consider". Or all types become DynSized which makes Rust removes a lot of its zero-cost abstractions.
  • Does not work for enum.
  • Does not work for ZST.

Store in the impl vtable

Pros

  • Works for all types.
  • Does not modify the "main" type.

Cons

  • Using the trait requires dyn Trait to use so that the extra data can have some per-instance place to actually live.
    • Which means that ty.uses_impl_var() is impossible and instead something like (Box::new(ty) as Box<dyn Trait>).uses_impl_var() is necessary. Note that "impl vars" would somehow have to be initialized in Box::new which means that traits which have "impl vars" need some additional information for Box to know about initializing them. Repeat for other containers which support dyn Trait in them.
    • This may mean that "impl var" is required to be const constructible (otherwise Box::new is of unknown runtime cost, not just "allocate and move")
  • Ambiguities are possible
    • Is self.impl_var a member of the type or the local impl variable of the same name?
    • Syntactic ambiguity: does accessing an "impl var" require different syntax to make it clear that the access has to happen in some other way?
    • What about super-trait "impl vars" of the same name?

As you can see, even these two (but by no means exhaustive) solutions have considerable downsides. Where you come in is helping to clarify what downsides you prefer (if they are at all acceptable in the first place). Otherwise you may be stuck with something that doesn't match what you actually want and everyone is sadder for it.

4 Likes

I'm not here to say it's easy - I know it would likely be a very big change (but overall concept is further hiding certain private variables (further protection) from other methods implemented on the struct - just moving them to a new spot, and out of view of everyone outside of this impl block, but they may be shared across all methods in the block. Lots to think about, I'm sure. Not as easy to "implement" as sounds, and lots of gotchas along the way. I'm just a high-level concept guy.

but here's very minimal example.

struct Foo  { ... some fields ...}
  
impl ICalcTotals for Foo{   // Start of the "impl block"
         partialTotal1: int32 = 0;    // Hi, I'm an "impl var"
         partialTotal2: int32 = 0    // me too
         
       pub calcSomething() {
                // calling methods outside of impl block is OK, they just can't access the vars defined in this impl block
                // You can pass it as a parameter tho of course, but they won't be able to change the value unless the method the parameter is passed to is defined in this impl block.
                 partialTotal1 += some_class.some_method();   
                 partialTotal2 += calc_partialTotal2();   
        }

        pub update_full_total(){
               this.total = partialTotal1 + partialTotal2;
       }
 }

You can already achieve the same privacy by using modules:

struct Foo {
    partial: partial::Partial,
    total: i32,
}

mod partial {
    pub(super) struct Partial {
        // private to mod partial
        total1: i32,
        total2: i32,
    }

    impl super::Foo {
        pub fn calc_something(&mut self) {
            self.partial.total1 += 1;
            self.partial.total2 += 2;
        }

        pub fn update_full_total(&mut self) {
            self.total = self.partial.total1 + self.partial.total2;
        }
    }
}

Let's see if I can pull this out of you, which of these impls should be allowed (if any)?

trait Foo {
    fn foo(&self);
}

trait Bar {
    fn bar(&self);
}

impl Foo for u32 {
    let x: u64 = 10;
    
    fn foo(&self) {
        println!("{}", self.x);
    }
}

impl Foo for std::ops::Range<u32>  {
    let y: &'static str = "hello";
    
    fn foo(&self) {
        println!("{}", self.y);
    }
}

// type from another user-defined crate
impl Foo for ::qux::Qux {
    let z: bool = false;
    
    fn foo(&self) {
        println!("{}", self.z);
    }
}

impl<T> Bar for T {
    let w: u8 = 100;
    fn bar(&self) {
         println!("{}", self.w);
    }
}

Please say the names of the implementor types as written

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