I thought method resolution also spanned multiple crates when generics were involved? In fact, I believe this is exactly why "the HashMap problem" is such a useful example to cite.
If you mean "just for the sake of method resolution in non-generic code", then... I think it's possible? Or since we're talking arrays, maybe const generics kills it again? Not sure it'd be useful at that point, but I'm out of technical holes to poke.
Could you provide an example? Sure if you have a generic type <T: Trait> then methods of the trait Trait will be available for any x: T, no matter the edition. But then again an fn f<T: Trait>(x: T) will currently not do any auto-ref/deref in a call f(x), so I see no problems here at all.
Edit: Of course this would create a weird situation in the old edition where an x: [i32; 10] could be passed to a function accepting IntoIter and even IntoIter::into_iter(x) might reasonably be allowed while x.into_iter() doesnât access that same trait impl. Iâm not sure how much rustc is supposed to support a 100% seamless programming experience in an old edition when a newer one is available. Possibly you could emit a warning in the old edition in code that directly makes use of the fact that SomeType: Trait even though that trait impl is âhiddenâ in that edition.
I think this would be ill advisable for the same reason that private names are visible to resolution logic.
It's supposed to be as normal as any other edition. Previous editions are still supported and things like NLL and other such improvements are available on edition 2015 because it's still a first class operating mode of the compiler.
Okay, then maybe the way to go could be: if you add a trait impl MyType: Trait to std that is marked as ânew in edition 202aâ and call a method m on an x: MyType, i.e. a method call x.m() from inside 2018 edition, everything would work the same in 202a and 2018 edition code except in the following scenario:
If (in 2018 edition code) both method resulution of x.m() with the new trait impl included and with the trait impl excluded succeed and resolve to something different, then x.m() gets resolved to the result of resolution with the new impl excluded, however also a warning gets issued stating that resolution ignored a particular trait impl for backwards-compatibility and a the warning includes a suggestion to change the call into Trait::m(x) to get the meaning you might expect.
So arrays implement IntoIterator, but calling its trait methods on arrays is forbidden in Rust 2015/18, so [].into_iter() is still equivalent to (&[]).into_iter() for backwards compatibility.
But then the following would work in Rust 2015/18:
fn call_into_iter<T: IntoIterator>(t: T) -> T::Iterator {
// At this point, the compiler doesn't know that T is an array
t.into_iter() // so this works
}
// Because of coherence, [] must implement IntoIterator in all editions
call_into_iter([]); // so this works
Yes, and even IntoIter::into_iter([]) could very well be allowed.
Yeah, the question is however not only about good or bad but really which is the best (or least bad) alternative. The current approach is well summarized by this commend on one of the RFCs on this topic:
I would argue that most solutions are better than to bend stability guarantees [...] no matter whether it breaks code. The current approach is just talking the problem away with statements like âit doesnât really make sense to call into_iter() when there is iter() for by-reference iterationâ or âRFC x1105 [...] specifies that [this] is a "minor change", [...] "this [...] breakage is considered 'minor'"â (relevant RFC).
Perhaps somewhat off-topic
The idea of hiding methods for resolution could even be applied to libraries, not just std. And I could imagine a similar approach to allow mitigate breakage that can arise from some::path::* imports, where an added item some::path::Item can create conflict and break code. How this could work: Have the compiler generate some kind of interface-file for each version "1.2.3" of a crate, then when compiling a crate with dependency on version "1.2.3", it could actually use a newer version like "1.3.4" but with everything new hidden to avoid breakage. With such compiler functionality at play the compiler could furthermore even produce an error when you remove things from an API in an incompatible way without incrementing the major version number.
This ties into the following question I have: Why is std having special powers in its ability to declare unstable API? And if anything comes out of the idea of gating std API on editions, thatâs another special power.
It could, but most crates have the luxury of simply releasing a new major version, so most of the reasons std might want this don't apply.
This sounds confused to me. The entire concept of "stability" and "unstable features" (at least the way I've always seen these terms used) only really applies to the compiler, standard library and associated toolchain. For regular crates on crates.io, experimental features can be expressed with other mechanisms like 0.x.y version numbers and cargo features, because regular semver logic applies.
I'm guessing an "unstable API" in a non-std crate would be one you can only use with a nightly toolchain (but I guess any nightly?). If so, why would a crate prefer this toolchain restriction over a 0.x.y version?
Admittedly there's also core and alloc within std, and there's been plenty of talk of splitting it up even more and making std just a facade, so it's not so much one crate being special as it is the crates shipped with, coupled with and versioned with the rustc compiler all being special by blurring the line between library and compiler.
I feel like this presentation is mixing up a bunch of issues in misleading ways and making "the current approach" / scott's position sound much more extreme than it is, so let's back up a bit.
The reason adding trait impls is a backwards compatible change even if it breaks code is because, fundamentally, we have to choose one of these:
Rust has type inference and adding trait impls is backwards compatible, therefore XIB (expected inference breakage) is allowed
Rust guarantees that adding trait impls is backwards compatible and XIB cannot happen, therefore we're not allowed type inference
Adding trait impls is not a backwards-compatible change, so Rust gets to have type inference and guarantee XIB cannot happen
A very long time ago (pre-1.0 I believe) we decided #1 was the least bad option. That doesn't mean "anything goes" for XIB, we still have to argue that it's worth it every time we add a trait impl to std, but it is allowed and actually happens on a regular basis.
Of course that's the simple version. Although this fundamental tradeoff is fairly clear, there's always going to be some amount of gray area in the precise wording of the stability guarantees around stuff like this. So out of context scott's quote risks coming off as "I don't care about our stability guarantees" when it's really much closer to "in this case, it is extremely worth it".
I like this! The compiler knows what the signatures/memory layouts of everything is, so it is in the perfect position to detect changes to public APIs. Would it be possible to make this available to all crates? Could crates.io have an automatic lint built in so that if you break the public API it forces you to bump your major version number? I know that it is always possible to break the public API in ways that can't be automatically detected, so this would only be a lint against obvious and egregious behavior, but it is a good first step.
Now thanks to editions there's another option, so maybe it's time to revisit this decision?
Rust is committed to stability, and this IntoIterator impl has the potential to break a lot of code. We can try to predict that with crater, but a lot of code isn't published on crates.io.
What is this fourth option and how do editions add it?
Since editions can only enable strictly "crate-local" and "surface level" changes, AFAIK pretty much anything to do with trait impls and method resolution is unchanged by their introduction (as the RFC talked about at length).
I'd assume it would be orthogonal to the toolchain version, but would be for unstable features of the crate itself, requiring some kind of opt-in like #![feature] and maybe a specially marked version of the crate.
However, I don't know if that would have much benefit over what Cargo features already allow.
This doesnât create a full new option for handling method inference breakage, my point there was only about std though (and editions only make sense to solve breakage in std anyways, since editions are like a new semi-major version of the rust language [that the standard library is part of]).
With âsemi-major versionâ I mean the situation which (as far as I can tell) does not fit 100% into semver, that you release breaking changes to an API but keep supporting the old API in a non-breaking way by making the switch to the new API opt-in. In Rust, we furthermore offer non-conflicting parts of the new API to code not opting into the new way. (Iâm using the term âAPIâ here, but in the case of the rust language this should be read as âsyntaxâ instead.)
Anyways, applying editions to method inference in std would shift Rust partially (i.e. in the context of the standard library) towards your second option, it would read roughly:
The Rust developers guarantee that adding trait impls to the standard library is only done in a backwards compatible manner and XIB cannot happen there by edition-gating (for the purpose of method inference) any trait impl that could cause breakage, therefore we're not allowed type method inference, even for the standard library, but the inference changes with editions.
Inference that changes with editions is kind-of scary, but with the correct set of warnings it probably gets well handle-able.
Taking this further than the standard library would be possible. It could be worth it for crates like data-structures whose types are publicly used in the API of other crates depending on them.
The data-structure crate could modify its API in a semi-major versioning manner, by (depending on how many features we add to rustc for this kind of stuff) adding breaking trait impls, adding things in general without causing any problems whatsoever with glob imports, hiding deprecated methods, possibly even re-claiming hidden methodsâ names for different purposes...
The library using the data-structure in its public API could switch to the new semi-major version without losing interoperability with other users of the same data-structure that didnât do the switch.
To mitigate the complexity of all of this, it would include an automatic check if there really is no breakage; activating this kind of versioning and stability checks for a crate would be optional. If activated, youâd suddenly get significant value of âunstableâ features in libraries since theyâd be allowed to change in a breaking way before reaching stability and a user would need to opt-in to use them.
I could go on, one could try to take this pretty far. Also sorry, I canât really put current cargo features into this context because I havenât really used them myself yet, so my understanding of the pragmatics around them are limited. And please keep correcting me about things that I call out as worse than they really are, or that have simpler solutions I donât know of that already exist.
I do believe that this could be turned into a full-blown âfourth optionâ, and it would be one with the goal of bending the curve to archive the convenience of inference, global imports, and fresh non-cluttered up-to-date APIs with, at the same time, stronger stability guarantees that make API breakage impossible, and keep dependencies seamlessly up to date which also helps with interoperability.
Interesting. Unfortunately we're at the limits of my type-fu now. I suspect that:
is not a guarantee we can actually make for any new trait impl (on existing types/traits), even if a user would have to write fairly weird code to make it happen. But I don't know how to prove or disprove that. Maybe you can guarantee it if there are zero generics in the impl signatures and none of the types are mentioned in any existing impls? (but wow that would be a super tight restriction)
Now that you're questioning this, I'm remembering that there's also potential conflict between different traits, lol. My assumption in my head was that potential for conflict is somewhat more limited, so that not every new trait impl needs to wait for the next edition. If this however is the case it might potentially be too much of a hindrance since new editions are not coming so often. Maybe not editions but rather rust releases are the better granularity, especially for mitigating breakage that rarely happens in practice; you would specify a version of std in the Cargo.toml or your crate and methods from a newer version than specified get less priority in resolution as well as generating a warning when such lower-that-usual prioritization applies. I'll have to think about this some more later, the trait problem is apparently more similar to the problem of glob imports that I initially thought. Also something I didn't mention yet is adding inherent methods to a type or methods to a trait. Edition (or version) gating could/should be applied to individual methods.
We could even make this a command-line switch; cargo check --edition_changes 2018 2021 or some such (precise arguments, syntax, etc., all subject to the usual bikeshedding).