Inconvenience of using functions defined in traits, an ugly workaround, and proposed solution

There is a usability tension between implementing a function directly on a struct, and implementing a trait for that struct that defines that method. In particular, when a function is defined through a trait (that isn’t in scope by default), we have to use that trait, but if we implement the function directly on the struct, we can call it without useing anything.

There’s an ugly workaround to avoid the need for callers to use the trait: define the function directly on the struct, and then implement the trait in terms of the directly-implemented function. See the code below, which is also available at https://is.gd/30Fo09.

mod traits {
    pub trait T {
        fn t(&self);
    }
}

mod inconvenient {
    pub struct S { }

    impl super::traits::T for S {
        fn t(&self) { println!("t()") }
    }
}

mod convenient {
    pub struct S { }
    
    impl S {
        // Allow caller to call `S::t()` without importing the `traits` module.
        pub fn t(&self) { println!("t()") }
    }
    
    impl super::traits::T for S {
        // Implement `T::t()` in terms of `S::t()` so they are consistent.
        fn t(&self) { S::t(&self) }
    }    
}

fn works_but_requires_use() {
    use traits::T;
    let x = inconvenient::S {};
    x.t();
}    

fn works_without_use() {
    let x = convenient::S {};
    x.t();
}

fn broken_without_use() {
    // This doesn't work because we didn't `use traits::T;`.
    let x = inconvenient::S {};
    x.t();
}

fn main() {
    works_but_requires_use();
    works_without_use();
    broken_without_use();
}

I would like to propose a solution:

mod more_convenient {
    pub struct S { }
    impl super::traits::T for S in scope by default {
        fn t(&self) { println!("t()") }
    }    
}

Note in particular the in scope by default extension to the impl. The idea is more_convenient would get desugared into convenient. In particular, one would be able to call methods defined in a in scope by default impl as if they were defined directly on the struct (because they are). Note in particular that the trait T itself is not brought into scope. That is, you could do this:

fn also_works_without_use() {
    let x = more_convenient::S {};
    x.t();
}

But, you can’t do this:

fn does_not_work() {
    let x = more_convenient::S {};
    <x as T>::t(&x); // `T` is not in scope.
}

I intentionally chose a syntax that nobody would like. I suggest we decide whether this is a good or bad idea without worrying about the syntax.

cc @nikomatsakis @withoutboats @sfackler (all of whom have discussed various ideas along these lines in the past).

[Let’s agree to disagree with respect to capitalization of type names here.]

I gave an abstract description of the problem, but I’ll also give two concrete examples where this has affected the API of the ring crate.

ring defines several kinds of (private) asymmetric key pairs, in particular RSAKeyPair and (soon) ECDSAKeyPair. Both RSAKeyPair and ECDSAKeyPair define a function with this signature:

fn from_pkcs8(input: untrusted::Input) -> Result<Self, error::Unspecified>;

I considered putting from_pkcs8() into a trait pkcs8::FromPKCS8 that both RSAKeyPair and ECDSAKeyPair implement. If I do that, then users of the crate will have to use pkcs8::FromPKCS8. But, if I don’t factor it out into a trait, then they don’t have to do that. In the end, I chose to not make the trait because the cost of requiring the use didn’t seem to justify the annoyance. I might implement the ugly workaround I mentioned above.

As another example, ring implements a ring::rand::SecureRandom trait and a ring::rand::SystemRandom implementation of that trait. In this instance I did use the ugly workaround so that people can use ring::rand::SystemRandom without a use ring::rand::SecureRandom;. (BTW, I’d like to encourage people to use the SecureRandom trait more and rely less directly on SystemRandom, so I might undo this to achieve that effect.)

Could it be accomplished by means of impl delegation (rfc #1406), i.e. filling two needs with one deed?

1 Like

In my opinion, there is a clear and long recognized need to solve this general problem. I've been wanting this feature, but I had thought of it differently. I wanted to have a notion of inherent traits. The idea was roughly that you could add "supertraits" to structs or enums:

pub trait Methods { ... }

struct Struct: Methods {
    f: u32
}

As a result of doing so, two things are true:

(1) We must be able to prove that every Struct instance has a Methods impl

(2) You can use the methods from the Methods trait as if they were inherent to struct.

My idea does not allow you to add "inherent methods" to Struct from outside the defining crate, however, whereas your proposed syntax might. This is also something I would very much like, though I had thought of it as an orthogonal problem. I am not keen on defining throw-away traits for that purpose, which is of course what you have to do now. I have previously proposed the idea of 'crate-local' inherent impls, but that had the downside that it could only work for inherent impls, not trait impls ("crate-local impls of traits" might be nice but have much broader complications) -- and, actually, inherent impls would maybe have some of the same complications if we applied specialization to them.

On a related note, I would eventually like the idea to put inherent methods right into the struct declaration. I'm not sure what purpose is served by forcing people to pull them out into a distinct impl block (mind you, I like the ability to put impl blocks in other places and add methods from all over, I just wouldn't mind a shorthand for the most common case):

struct Foo<T: Ord, U> {
    fields...;

    fn bar(&self) { ... }
}

It's a bit awkward, because fields are comma-separated, though no more so than the proposed field-in-trait-syntax (really have to get back to that RFC...), not entirely by coincidence. You could imagine extending this notion to impls of traits, which might then also make the trait impl inherent. In this case, condition (1) above doesn't apply, because we're providing the impl right there. The downside of course is the rightward drift.

struct Foo<T: Ord, U> {

    impl Eq {
        fn bar(&self) { .. }
    }
}
2 Likes

That said, I don't think there's a fundamental reason for this requirement. I think I wanted it because of the syntax being so suggestive of supertraits etc. It would seem weird to write struct Foo<T>: Methods but then have Methods only implemented for Foo<T> where T: Copy. But then again, that's something that's readily possible with inherent methods, so maybe it should be allowed

In general, the idea of making the impl be default side-steps some of these issues, and has some appeal to me.

So if the impl Trait {... syntax were allowed inside impl Struct blocks, then there would be zero ambiguity and confusion:

impl<T: Copy> Struct<T> {
    impl Eq {
        ....
    }
}
1 Like

I don't think there is either ambiguity or confusion in either way -- that is, the meaning of putting things in the struct is clear, but it's not as flexible as one might like -- but I think that's an interesting idea nonetheless! (I might even support combining it with the ability to put an impl in the struct itself, or maybe that's getting way too many ways to do things.)

One thing I like about putting inherent methods in the struct itself is that it avoids the need to "repeat yourself" about the set of type parameters and so forth.

This “feature” wasn't something I intended to enable. i think it makes sense to restrict “inherent” implementation of traits to the same places where one could add a method to a type (same crate? same module?). In this sense, what I'm proposing is nothing that couldn't be done more verbosely using the ugly workaround I described.

I like your idea of "inherent traits" quite a bit. I'm unsure whether it is a good idea to have two different ways, syntactically, to implement a trait for a type.

I used to hate this, but now I am fine with the way things are now. For a beginner it seems strange to require the type parameters and whatnot to be “repeated”, but really nothing is “repeated” since the parameters aren't necessarily the same. Plus a good editor should spit out all the boilerplate for implementing a trait with a few keystrokes or less.

So @withoutboats and @eddyb were pretty negative on IRC about the idea of functions and things within struct definitions. Which I totally get (and, to be honest, expected, but I figured I'd throw it out there).

Anyway, if we scale back the "inherent traits" concept to its essence, it was the idea that one could add "supertraits" to a struct declaration:

struct Foo: Trait<...> {
}

I still like this syntax quite a bit. I do wonder though how often we would want however the added flexibility that a "default impl" brings.

That said, once the "chalkification" process is complete, it seems pretty plausible to have "conditional" where-clauses of the form (T: Copy => Trait<...>). We may well want this for things like impl Trait and so forth (i.e., so you can say something crazy like impl (Iterator<Item=T> + (Self: IndexedIterator => IndexedIterator)), meaning "this is an iterator -- and, if Self is an iterator, then it must implement IndexedIterator too.

To clarify, I prefer the syntax:

impl Foo {
    pub use Trait::*;
}

But I’m not sure it’d ideal either.

2 Likes

Using impl delegation:

/// Optional doc comment.
impl Trait for Foo {
    use self;
}
2 Likes

I think maybe we should workshop some solutions for these kinds of delegation things at a lang team meeting. This design space seems hard to break down… there are a lot of options and nothing in the language as it exists provides much guidance toward what we should do.

Maybe there could be cases where it “just works” without extra syntax? e.g. if the struct and trait are defined in the same crate/module.

1 Like

I don’t think this idea and delegation are the same and I don’t see much benefit in unifying them into the same concept. It is true that the ugly workaround that I described is a form of delegation. However, syntactic sugar for that ugly workaround is only slightly less ugly than what’s possible now, because “delegation” doesn’t match the mental model (intention) of the programmer.

I also like @kornel’s idea that we shoujld explore making it “just work” at least in the case that the X and impl Trait for X are in the same module/crate.

Regardless, note that there needs to be some way of indicating in the API documentation for a type which trait implementations are “inherent” and which require useing the trait.

There seems to me to be two issues here: firstly, the impl side where I’d promote impl delegation, and secondly, the use side which I think can be made orthogonal to how the trait is implemented.

The “just works” without extra syntax, proposed by @kornel, is attractive even though I’m wary of all things implicit.

I feel more strongly about the impl side because Rust has already taken a stance against the class-like amalgamation of data and methods. I quite enjoy the separation of concerns that trait impls enforce today.

What do you do if you have overlapping inherent traits?

Why not just make it so that if the trait impl is in the same crate as the target type, then it is considered as if the trait were imported, without requiring any additional syntax?

I don’t see the point in manually marking “inherent traits” as opposed to just making all those implemented in the same crate automatically inherent.

We can't always do that. There may be ambiguity (e.g., two traits that define a method with the same name -- both cannot be inherent). Right now you can select which one you want by importing only the designated trait (or by using the fully explicit syntax Trait::method(rcvr, ...)) .We could in theory do this if there is no ambiguity, though.

(Question: what about things like impl Trait for &Type? Would the "auto-inherent" would apply specifically to impl Trait for Type? If not, what other types would be covered.)

Yes. I imagine we would want to present the methods amongst the other inline methods just as if you wrote wrappers.

If I understand correctly, wouldn’t an automated solution that’s based on if the impl is in the same module also mean that I’d have to specifically move trait implementations elsewhere to be sure not to introduce a new API at a distance by accident?