Evolving the standard library through ... what?


#1

Hi there!

The Rust core language is extended pretty quickly (in comparison to many other languages) and we regularly get new type-system features or new syntax. Some, if not many, of those changes affect API design: with a specific feature, one can build a better API than without that feature.

The standard library’s API is not different in this regard: pretty much the whole API was designed for the Rust 1.0 core language and new core language features could improve the API in several places. To give a few specific examples:

Possible API improvements
  • with generic associated types, one could improve the Deref trait to make it more generic and powerful:
    trait Deref {
        type Target<'a>;
        fn deref(&self) -> Self::Target;
    }
    
  • with the impl Trait feature, one could hide a lot of implementation detail (maybe return impl Iterator instead of Map in Iterator::map())
  • with GATs (again), one could write collection traits which would potentially influence the API of all collections

(However, I think everyone agrees that possible improvement exist; concrete examples are not important for my question)


Of course, all those examples are backwards incompatible. That’s a problem, we don’t like to break users code; we promised stability. So we can’t simply use those improvements.

As probably everyone here is aware, there is the “Evolving Rust through Epochs” RFC. It specifies how we can change most of the Rust core language in a backwards incompatible way while avoiding breakage as well as a community split. It mentions the standard library at two points:

  • “Epochs are designated by the year in which they occur, and represent a release in which several elements come together: […] The standard library and other core ecosystem crates have been updated to use the new features as appropriate.”
  • “More generally, breaking changes to the standard library are not possible.”

These statements seem to contradict each other. I wonder if/how it is possible to introduce breaking changes to the standard library. I think that breaking changes are required, because IMO regularly updating the std’s API to use the core language as idiomatic as possible is a good thing. I just never heard anything about these kinds of std changes from the Rust community.

In Summary

  • I think we should update std (from time to time!) in a backwards incompatible way to use new language features in order to improve stds API.
  • In my naive worldview, std is simply a library crate and we could bump the major version while the old version is still available (just like epochs).
  • The epochs RFC even manages to introduce breaking changes to the core language, so I feel like it should be certainly possible to introduce breaking changes to std.

Are there plans on how to introduce breaking changes to std? Or is it impossible to do? Why?


Sorry if this has been discussed before, I haven’t found any previous threads. I asked a similar question on StackOverflow (without answer).


#2

All of your dependencies would have to be updated to the same version of std as you, because an Option from an std on the old epoch isn’t the same type as an Option from the new epoch.


#3

There is a workaround for that. std2 can pub use everything from std1 that it doesn’t modify.


#4

Users would have to then switch all of their std imports to std2. We can’t reuse names that are already defined. At that point, why not just do Index2

Unless we have mutually exclusive traits (which is a whole other set of problems), only one trait can control the index operator, otherwise its unclear what happens when you implement both.

In theory, the best way to do this would be to create Index2 and provide this impl:

impl<I: Index<T>, T> Index2<T> for I {
    type Output<'a> = &'a <I as Index>::Output;
    fn index(&'a self, idx: T) -> Self::Output<'a> {
        <I as Index>::index(self, idx)
    }
}

Then make Index2 control the definition of Index. The churn of having 2 different ops traits with slightly different definitions seems quite problematic to me though.


This also has the weakness that IndexMut currently relies on the ref requirement to return an &mut <Self as Index>::Output. We don’t have built in mutability polymorphism, so other than hardcoding it to & and &mut, there’s no way to guarantee that IndexMut returns “the mutable form” of Index.


#5

Mh ok, so there are basically the same problems as with any other library: if you have two different versions of a library in your dependency graph, it will be complicated, right?

Has there been previous discussion about this? If someone could provide some links, that would be awesome! Or is it just widely accepted that we will never introduce breaking changes to std? Sorry if these questions sound a bit dumb, but I either missed all of those previous discussions or I’d be very confused why no one thought about it :confused: