Pre-RFC: Absolute minimum for in-impl imports

I hear people complain that having to pull in random traits to access core functionality of certain types all the time, and I think this is one of Rust's biggest usability issues (prelude modules are a ridiculous workaround). Hopefully this is the minimum proposal that can solve this rough edge once and for all.

tl;dr use items are permitted in impl blocks for pulling in trait methods.

Motivation

Suppose I have a type whose sole purpose is to implement some trait, like one of the byteorder traits. I should not have to write use byteorder::ByteOrder as _; to access those functions. Another good example is arena allocation APIs in my library, which results in use ArenaExt as _; in code that manipulates an A: Arena; this is a fairly common pattern in other code I've encountered.

Non-goals

  • Solving general delegation, e.g. delegating a method to a field (e.g. see the by keyword in Kotlin). This is a good problem to be solving, but I think this issue of trait imports is a bigger useability issue for library consumers.
  • Solving the general "my library has a prelude" problem; this merely makes it so that preludes that import a bunch of traits to make its own types' methods visible are unnecessary.

Guide-level explanation

If I have

struct K;
trait T { fn foo(&self); }
impl T for K {
  fn foo(&self) { println!("bang!") }
}

I am entitled to write

impl K {
  use T::foo;
}

This is syntax sugar for (roughly)

impl K {
  #[inline(always)]
  fn foo(&self) {
    <K as T>::foo(self)
  }
}

Associated consts may be imported, and, if the relevant feature gate is enabled, so may associated types.

This feature may also be used in trait impls:

trait U { fn bar(&self); }
impl U for K {
  use T::foo as bar;
}

In the standard library, we could write

impl Formatter<'_> {
  use std::fmt::Write::{write_fmt, write_str};
}

(Currently, the impl for Formatter simply contains these two methods verbatim in its body to make write! work properly without trait imports.)

Reference explanation

We permit use items in impl blocks, where path(s) in the item refer to trait items, such as trait methods, associated consts, or associated types. Each path is expanded into a copy of the referenced trait item, with the exact same signature as in the trait, but potentially renamed with as, as with any regular use. use Trait::*; will import all items.

The visibility specifier on the use item is applied to each item imported.

use items' paths may contain type parameters. Currently use Foo<Bar>; is a parse error, but I see no reason why use cannot consume a general path. For example, you could use use AsRef<[u8]>; in an impl. Type/lifetime parameters of the impl are visible to the use items.

It is an error if:

  • The path does not resolve to a trait item.
  • The compiler cannot prove that the implementee type does not implement the trait in question, given the bounds on the impl block.
  • The imported name clashes with an existing definition in the impl block (i.e., as if the method had been defined directly, as in the pseudo-desugaring above).
  • When importing names into a trait impl, the resulting imported definition does not match any of the trait-being-implemented's requirements.
  • Using a visibility specifier on a use item in a trait impl; it is always implied to be pub use, as with all other trait impl items.
  • Drop::drop may not be used, because Drop Is Special.

For example:

trait T { fn foo(&self); }
trait U { fn bar(&self); }
trait V { fn foo(&mut self); }
struct K;
impl T for K { .. }
struct L;

impl K {
  use Vec::new;  // Error, not a trait item.
}

impl K {
  fn foo(&mut self);
  use T::foo;  // Error, redefinition of `foo`.
}

impl L {
  use T::foo;  // Error, L: !T.
}

impl U for K {
  use T::foo;  // Error, no such declaration U::foo.
}

impl V for K {
  use T::foo;  // Error, wrong signature for V::foo.
}

impl<X> U for X {
  use T::foo as bar;  // Error, cannot prove for<X> X: T.
                      // Would be fine given a blanket impl for T.
}

Note that the desugaring given in the guide-level explanation isn't quote correct; imported methods and consts are true aliases and guaranteed to have the same address.

Open Qs

  • Should we be able to write impl K { use T::*; } even if T has associated types? (This would have the effect of not importing associated types until inherent-assoc-types is stabilized.)
  • How should this be displayed in rustdoc? How does this interact with the "important traits" feature? (I think that any item imported in this manner into an inherent impl should be noted as having come from a trait in some manner.)
  • You couldn't write something like use T::foo where Self: T; I don't see a reason not to allow this other than the funky syntax, but it's probably best left as a future extension.
  • Should we punt use foo::Foo<T>; to a future extension?
  • How should this interact with trait aliases, if at all?
  • Should we describe an extension where, given impl<X: T> U for X { .. }, we can write trait T { use U::*; ... }? This would be useful for making extension traits part of the overall trait, such as in my Arena/ArenaExt example above.
  • Should use Struct::method; be permitted in trait impls? I.e. the dual of this proposal. Note that it is never valid to use this inside of an inherent impl for obvious reasons.

Prior Art

Best I can tell, this is almost identical in spirit to C++'s class-scope using declarations. In C++, you can write using Base::Foo; in a derived class to make Foo visible as a derived class member (there are some caveats around inherited name lookup that, thankfully, are irrelevant in Rust).

2 Likes

Crates providing this functionality:

  • dtolnay's inherent — one visibility for all trait items
  • IdanAyre's inherent-pub — visibility per each trait item

Honestly, I could get behind inherent-pub's semantics being part of the language. That is, specifically, if you put a visibility modifier on a trait impl's item, it's also available as an inherent item with that visibility.

I think there's definitely appetite for something here -- things like using BufRead on BufReader without needing to use the trait seems like a clear win.

There's even a proposed RFC for it, that came up recently in the libs and lang team meetings:

2 Likes

It’s interesting that we sort-of have a similar mechanism already. For fn f<T: some::Trait>() you can call methods on T, despite the trait not being in scope.

8 Likes

Fascinating. I didn’t know this was a thing, the rules seem to be a bit arbitrary though…

mod m {
    pub trait Tr {
        fn foo();
        fn bar(&self);
    }
}

struct S;
impl m::Tr for S {
    fn foo() {}
    fn bar(&self) {}
}

fn f<T>(t: T)
where
    T: m::Tr,
{
    // S::foo(); doesn’t work
    T::foo();
    t.bar();
}

fn g(s: S)
where
    S: m::Tr,
{
    // S::foo(); doesn’t work
    // s.bar(); doesn’t work
}

struct W<T>(T);

impl<T: m::Tr> m::Tr for W<T> {
    fn foo() {
        S::foo(); // works
        T::foo()
    }
    fn bar(&self) {
        S.bar(); // works
        self.0.bar()
    }
}

fn h<T>(w: W<T>)
where
    T: m::Tr, // either of these or even both bounds
    W<T>: m::Tr, // gives the same behavior
{
    // W::<T>::foo(); doesn’t work
    // w.bar(); doesn’t work
}

trait WithAssoc {
    type Assoc;
}

fn i<A>(a: A::Assoc)
where
    A: WithAssoc,
    A::Assoc: m::Tr,
{
    // A::Assoc::foo(); doesn’t work
    // a.bar(); doesn’t work
}

fn j<A, T>(a: A::Assoc)
where
    A: WithAssoc<Assoc = T>,
    A::Assoc: m::Tr,
{
    A::Assoc::foo();
    a.bar();
}

This hits a different case -- within trait impl, the trait itself is considered in-scope.

Sure, I figured as much. Another interesting test:

trait Foo: m::Tr {}

fn k<T: Foo>(t: T) {
    T::foo();
    t.bar();
}

fn l(s: S) {
    // S::foo(); doesn’t work
    // s.bar(); doesn’t work
}

So a T: Foo bound brings into scope supertrait methods, too, whereas ordinarily, just Foo being in scope doesn’t allow access to supertrait methods.


And here’s some more variation on f

fn f<T>(t: T)
where
    T: m::Tr,
{
    // S::foo(); doesn’t work
    T::foo();
    t.bar();
    T::bar(&t);
    <T>::bar(&t);
    // <_>::bar(&t); doesn’t work
}

I don't think that's a good semantics:

  • It's kind of a spooky action at a distance.
  • It's not at clear all what pub(crate) should mean in this context.
  • It doesn't work if another crate defines a blanket impl for you.
  • It does not allow you to potentially do the opposite: pull items out of a different impl into a trait impl, which would be useful for users implementing a crate for an upstream type.
  • It does not allow you to do renames.
1 Like

"Inherent trait impls" makes us think of our own crate, impl_trait, not this.

The reason it's called inherent trait impls is that the trait impl functionality is exposed as inherent on the type, without importing the trait.

Yours would be inherent impl trait (to be maximally ambiguous) or just impl trait in inherent impl.

1 Like

Why not merge them both? (And improve the way they show up in docs while at it - if they're inherent, they should probably show up in docs.)

Is there a reason why you need to import the traits at all?

Could we not change the method resolution to the following:

  1. Methods in impl T
  2. Methods in impl Trait for T where Trait is explicitly imported
  3. " " " where Trait is glob imported
  4. " " " where Trait is prelude imported
  5. " " " where Trait isn't imported

Up to step 5 I think this is how it currently works. So I don't think adding step 5 would break any current code.

That's not quite right -- I think 1-4 is correct for everything except trait methods, but the reason adding TryInto to the prelude ended up being an edition change is that at least 2 & 4 conflict today.

I understand that it is an edition change. But I don't see how 2&4 conflict?

If I import a trait, does that not guarantee that it overrides a prelude import?

Imported trait methods do not take precedence over predule trait methods.

1 Like

Ah I see, is there any documentation as to why?

Could this be a edition change?

If this were not an error, then implementing a trait on a type could silently change the meaning of existing code, making it a breaking change. (Though if the change were limited only to the prelude, rather than glob imports in general, this would only affect traits with method names that conflict with the standard prelude traits.)

1 Like

How would it be a breaking change to implement a trait? If that trait was not previously imported then the prelude trait would be used instead.

It could be a new impl of an already-existing trait on an already-existing type.

(I certainly admit that it takes very special circumstances for this to break code in practice.)

Thanks for the pointer. I will play around with this to find the example, because I cannot figure it out off the top of my head.