Adding `what` parameter to #[deprecated]

Abstract

it'd be nice to be able to specify what exactly is deprecated as opposed to deprecating all instances of an item. It could be done by supplying what = "value" where value could be:

  • impl - implementation of this trait or trait method is deprecated but calling the method or using trait in bounds is fine
  • call - calling this trait method is deprecated but implementing it is fine
  • bound - using this trait in bounds is deprecated

Use cases

Error trait

std::error::Error has cause deprecated as it got replaced by source However source is not present in old version of std. So a crate that intends to support old versions of Rust has to call cause and get a warning in the new versions of Rust. (This is a real case in the slog crate.)

This situation is not ideal because calling cause() should be perfectly fine. It just forwards to source. implementing cause() is problematic without source() and redundant if source is present. Thus it'd make more sense to only warn when cause() is being implemented, not when it's called.

Using #[deprecated(what = "impl")] on cause() would tell the compiler that implementing the method is deprecated but would not lead to warnings if cause() is called.

From/Into

Since From implies Into, types implementing Into are superset of types implementing From As a result, it makes more sense to implement From and bound by Into or call into(). These attributes could warn users about this being the case:

#[deprecated(what = "bound")]
trait From<T> {
    // I think what = "bound" should imply deprecated what = "call" on each trait method
    // But for illustration purposes I wrote it explicitly
    #[deprecated(what = "call")]
    fn from(val: T) -> Self;
}

#[deprecated(what = "impl")]
trait Into<T> {
    fn into(self) -> T;
}

#[allow(deprecated)]
impl<T, U> Into<U> for T where U: From<T> {
    fn into(self) -> T {
        T::from(self)
    }
}

This would remind people to do the right thing.

Low level and high-level methods

Consider a hypothetical trait:

trait Foo {
    #[deprecated(description = "use high_level instead" what = "call")]
    fn low_level(&mut self);
    // #[deprecated(what = "impl")] intentionally not used because allowing specialization is useful
    fn high_level(&mut self) {
        // do some other logic

        // #[allow(deprecated)] could be implicit in this specific case
        self.low_level()
        // perhaps more logic
    }
}

This would help people understand the meaning of the methods better. One example of a trait like this is std::io::Read however I'd not sugggest to add it to that one since raw read() is still widely used. OTOH, maybe actually warning newbies about it is a good thing and slapping `#[allow(deprecated)] to mean "I know read is not guaranteed to read whole buffer" may be fine. That being said this debate is out of scope of my post. For now.

Helper marker traits

arse_arg has two similar traits: ParseArg and ParseArgFromStr. ParseArgFromStr requires FromStr and implies ParseArg. This API makes it possible to implement ParseArg for PathBuf (without causing issues with encoding) while not burdening implementors of ParseArg too much if the type already implements FromStr. A similar approach is used in embedded-hal.

A downside of this is there's a risk that someone will misunderstand the purpose of those traits and bound by ParseArgFromStr by accident. Adding #[deprecated(what = "bound")] to ParseArgFromStr would help mitigating this risk.

What do you think?

Is it worth the effort to do RFC for this? Do you see any problems with this idea?

But we do want to deprecate calls to cause() and have people use source() instead!

2 Likes

I strongly prefer the current behavior of deprecated. We are trying to gently push everything towards nondeprecated things. The intermediate remedy of sprinkling #[allow(deprecated)]'s around the project is not awful, but could be better. I believe I have seen a proposal on here for some wider scoped allow deprecated with a target, which also sounds fine and covers your case.

@sfackler do you expect Rust 2.0 to come out anytime soon? Because if not, I don't see a good reason to deprecate calls to cause().

Anyway, that's just one of the use cases. Another crate could have a different policy than std so the feature can be useful for other crates even if it doesn't end up in std.

1 Like

No, it may very well never happen.

Deprecation doesn't necessarily mean that it will be removed. Many deprecation's are because something is replaced with a better alternative (try! is replaced with ?), is easy to misuse (mem::uninitialized was often used in ways introducing UB), or is unsound (CommandExt::before_exec is missing unsafe, so has been deprecated in favor of pre_exec which is unsafe) In all cases, we can't remove them due to the stability guarantee.

In the case of cause() vs source(), cause() simply couldn't be used for some of it's intended purpose as it was missing + 'static. Not deprecating it could easily confuse someone trying to use cause() when they need to use source().

1 Like

The entire point of deprecation is to avoid needing a breaking API change to get people to stop using something.

9 Likes

Not deprecating it could easily confuse someone trying to use cause() when they need to use source() .

Interesting point of view. Wouldn't that mean that it could be reasonable to deprecate Rc because people might be confused and they really need Arc? :wink:

I think cause() still may have its place for people who don't need down casting. And FTR, downcasting may be one day implemented for non-'static types.

The entire point of deprecation is to avoid needing a breaking API change to get people to stop using something.

AFAIK it's formally breaking even if you don't know of anyone using it. And I think it's not unreasonable to use cause() if you're dealing with an old code or some interesting policy requiring older (certified?) version of the language.

Anyway, we're still talking about this one thing. What do you think of the other use cases?

Isn't Rust 2018 basically Rust 2.0? If not, then what do people mean by "Rust 2.0"?

Rust 2.0 would allow major breaking changes, like removing mpsc from std or changing the signature of Write::write to allow uninitialized buffers. Things that will never be done on an edition change.

4 Likes

So that's really std 2.0, right? "Rust" means many things, and Rust 2018 already introduced breaking syntactical changes. So we already have a "Rust language 2.0". Just not a "rustc 2.0" or a "core/std 2.0".

"Rust" is used as an umbrella term for many things, so thank you for clarifying the specific usage here.

Rust 2018 only introduced breaking syntactical changes. It doesn't change anything to the core language. Almost all differences are gone at HIR level. Rust 2018 has the same semantics as Rust 2015. It only presents them in a somewhat different syntax. This is the reason that Rust 2015 and Rust 2018 crates can be used interchangeably. As the only changes are syntactical and not semantical, I wouldn't call it "Rust language 2.0". I would rather call it something like "Rust syntax 2.0".

9 Likes

Rust and std are so deeply connected that they share the same version number. I guess they could be a bit disconnected so that e.g. new std features would be usable in old Rust compiler but that's a different discussion.

So by Rust 2.0 I mean big breakage, not edition. As explained by others, editions can't remove methods from traits.

source() is intended to be a complete replacement for cause(). There's no "use this if you need this, use that if you need that". The old method had a bug in its signature, and this is the fix. There's only one supported method now.

Arc is not a bug fix of Rc, but a different type with different use-case. They're meant to coexist, unlike cause() and source().

No they can't be. std has special handling in rustc, using features that are tied to the specific release.