'impl chaining' allows declaring impls on items without repeating the type variable declarations.
Generics with bounds can become rather verbose. Consider this example:
use std::ops::Index;
use std::marker::PhantomData;
pub struct TypedArray<IdxIn, IdxOut, T>
where
T: Index<IdxOut>,
IdxIn: Into<IdxOut>,
{
val: T,
indexers: PhantomData<(IdxIn, IdxOut)>,
}
// 'free impl'
impl<IdxIn, IdxOut, T> Index<IdxIn> for TypedArray<IdxIn, IdxOut, T>
where
T: Index<IdxOut>,
IdxIn: Into<IdxOut>,
{
type Output = T::Output;
fn index(&self, index: IdxIn) -> &Self::Output {
&self.val[index.into()]
}
}
This proposes some syntax to sop up the un-DRY repetition:
use std::ops::Index;
use std::marker::PhantomData;
pub struct TypedArray<IdxIn, IdxOut, T>
where
T: Index<IdxOut>,
IdxIn: Into<IdxOut>,
{
val: T,
indexers: PhantomData<(IdxIn, IdxOut)>,
}
// 'chained impl'
in impl Index<IdxIn> {
type Output = T::Output;
fn index(&self, index: IdxIn) -> &Self::Output {
&self.val[index.into()]
}
}
The feature is called "impl chaining", and the in impl item is a "linked impl". The original impls are hereby dubbed "free impl".
As the compiler parses the module or block expression, the type variables of the most recently defined enum, struct, or type alias is kept in memory.
(eg, Option<Scope>)
Certain items reset this state, and some items do not affect it.
Functions and modules reset it.
Imports do not reset it ???
Variable, constants, and statics reset also?
A linked impl can of course follow another linked impl.
They can also introduce new type variables, and add additional bounds,
but these changes are scoped only to that specific link, and is not visible to any following links.
If the user wants to add a bound used by multiple impl links,
they can set up the desired scope by adding a (possibly non-empty!) free impl.
I think with impl would look the most natural, however this (probably) has ambiguities when parsing block expressions, and impl non_keyword would break compatibility. in and for are the only keywords that could possibly make any sense for impl chaining, so that gives these options:
in implimpl infor implimpl forWhat names and terminology work best for these concepts and why?
How is this idea best presented鈥攁s a continuation of existing Rust patterns, or as a wholly new one?
Would the acceptance of this proposal change how Rust is taught to new users at any level?
How should this feature be introduced and taught to existing Rust users?
What additions or changes to the Rust Reference, _The Rust Programming Language_, and/or _Rust by Example_ does it entail?
Old code would like to be changed to use this.
It makes the language more complicated.
This might encourage sloppier code. It could encourage proliferation of unnecessary bounds, because a struct can have laxer bounds than an impl.
It might encourage re-arranging the file in less-than-ideal ways.
The example doesn't seem very convincing to me. Maybe there there aren't any examples that are really all that convincing.
Maybe the pain of it will encourage the production of Very Fancy Rust IDEs.
I was surprised to learn that you can do a free impl on a type alias.
Maybe other people would be surprised by this and inadvertently chain an impl to a type alias.
What other designs have been considered? What is the impact of not doing this?
struct Thing {
field: i32,
fn new() -> Self {
Thing { field: 5 }
}
}
This is not as powerful, but it is simple & what other languages do.
What parts of the design are still TBD?
Better names?
This sounds a lot like the "implied bounds" idea discussed in https://internals.rust-lang.org/t/lang-team-minutes-implied-bounds/4905
@Ixrec In particular, I wonder if implied bounds would make impls sufficiently less verbose to look a lot like the proposed syntax minus the in.
My main concern with something like this is that it might make possible to accidentally impl a method or trait for the wrong struct, which would lead to either really confusing compile errors or incorrect code, especially in a large file with lots of code where the struct and the chained impl are not both visible together.
Would these cross crate boundaries? Can this permits foreign impl when they depend upon private traits?
_Crate 1:_
trait Foo { ... }
impl<T> Foo for T where T: Iterator { ... }
pub struct B<T> where T: Foo { }
_Crate 2:_
trait U { }
impl<T> U for B<T> { }
Amusingly one cannot determine for which T this impl<T> U for B by reading the only the source of crate 2 and generated documentation of crate 1. As Foo is private you must read the linked source for crate 1 too.
There are likely many situations where one might want the generated documentation to include private items, so this does not sound problematic.
@burdges I don't think that is specific to this proposal; i.e. you can have the same problem in existing rust, right? Also, I think this is the opposite case from what I mentioned above. In your example, U is implemented for fewer types than one expects because T is restricted in crate 1. On the other hand, my concern is that you could move a struct declaration somewhere new, and all of sudden, the chained impls now belong to a new struct.
Afaik there is no "problem" in the case I noted, merely an observation that local traits can now be impled for a foreign struct where they could not before. I do not quite understood your concern yet.
Ah, I now see what you meant. I guess it could be viewed as either a good thing or bad thing... I am not sure what to think yet...
Regarding my concern... It's not really a fundamental problem with this proposal, I think -- just something that needs to be worked out. Imagine you have a large file with lots of code:
// some stuff
// Some struct
struct Bar;
// Another struct
pub struct Foo(...);
// A bunch of chained impls
// Now impl a marker
in unsafe impl Send {...}
// some more stuff
Now imagine that I want to clean up the code, so I move struct Foo somewhere else... but now struct Bar impls Send!
To be fair, I expect that in almost all cases this would result in the program not compiling, but then again, the compile errors might be a bit confusing at first. Plus, fixing the errors would mean moving all of the chained impls or unchaining them, which sounds tedious.
@mark-i-m That's exactly the kind of concern I had.
I think if we fix some other open issues that cause you to have to repeat the type constraints in the first place, this will become less of an issue.
I see. I got distracted by implied bounds and did not reread the proposal, sorry.
I think your objection sounds fatal to this proposal for regular structs. I suppose this can happen with if .. {} if .. {} else {} in badly formated code, but we've strong conventions against bare else { } blocks. You could maybe salvage this proposal by restricting to tuple structs which require a ; so that it at least does not require inventing conventions like those against else { }.
You might be able to save it with a tad bit more boilerplate:
struct Foo;
struct Bar<....>(...);
// some impls
in unsafe impl Send for Bar{}
But overall, I am starting to feel like @joshtriplett...
@burdges That's true. For me, though, I think the main thing is that most programmers who move parts of a function around tend to be pretty careful about how it changes the functionality of code (at least, I do :stuck_out_tongue: ). In contrast, moving around seemingly complete blocks of code like a struct definition don't usually inspire me to re-read the whole file...
@mark-i-m
That's why you should use if-style indentation
struct Foo;
struct Bar<...>(
...
) in impl Deref {
type Target = Baz;
fn deref(&self) -> &Self::Target { ... }
} in impl DerefMut {
fn deref_mut(&mut self) -> &mut Self::Target { ... }
} in unsafe impl Send {
} in unsafe impl Sync {}
Most helpful comment
This sounds a lot like the "implied bounds" idea discussed in https://internals.rust-lang.org/t/lang-team-minutes-implied-bounds/4905