Traits as contract, without changes to call-sites


#1

What I’m trying to do is to define a trait as an API contract and use it as a marker on some types to enforce their behavior.

Let’s say we have this:

pub trait MyContract
where
    Self: Copy + Debug + Display + Eq + Hash
{}

impl MyContract for MyEnum {}

This works, but it does not enforce function implementations for the type MyEnum, but only the traits specified.

Now, let’s say I want to enforce fn foo() to be implemented by MyEnum. If I put fn foo() in the trait body, then every user has to import MyContract before being able to call into MyEnum::foo(). If I define a helper trait as the contract, again it’s possible to add Self: HelperContract to the where clause, but, again, user is mandated to import HelperContract before being able to call to the function.

We don’t want that, because it makes the API way more complex to use, and it’s kind of misleading, because here, MyEnum is a MyContract, and foo() is considered native to MyEnum and not a side behavior of the type.

I can see that the syntax of where clauses do not allow bounding to function implementations on types. So, I’m wondering if there is an alternate solution to this problem? What would you do for such an API?

Also, could there be an extension to Rust’s where clauses that would allow bounding based on Self implementations, instead of Trait implementations?


#2

AFAIK, the reason why traits have to be explicitly brought in scope is that nothing prevents two completely unrelated traits from providing a method with the same signature and diverging semantics, and when you do something like x.foo() the compiler has to choose or request a disambiguation.

Sometimes, crate authors use a “prelude” module which groups together all the things that they expect every user to import in scope. This is notably used to put crate-specific traits in scope, in order to avoid having to import them manually. A user would then only have to do:

use your_crate::prelude::*;

Could this work for you?


#3

Thanks, @HadrienG. Right, prelude technic is one way some crates get around this problem. But I’m not a big fan of it, because it hides away the naming even more than before, and is more like “copy this line and some magic will happen”. Therefore, I’m hoping to avoid it.


#4

Personally, I won’t have anything more for you at the moment, but maybe someone else will.

I can understand the choice of making trait methods live in isolated namespaces that have to be brought in scope, as one of the main problems with duck-typed interfaces (as in C++ templates, Python code and Go interfaces) is that it’s very easy to have two independently created interfaces using identically named methods with different semantics. That’s particularly a problem with common method names like new() / open() / read() / write().


#5

My first thought was that traits are designed to do what you want, and you should just use them as they are designed to be used. And I still think this is your best plan with the current language, since it is how rust is designed.

However, I’m wondering whether a mechanism could be added to “bundle” a few traits with a given type. Bundling in this way could only be done in the crate that defines the type itself. This would treat the methods of those crates as “in scope” for that type whenever that type is in scope. I would love this in the standard library, particularly for std::fs::File, which I never want to use without at least std::io::Read or std::io::Write. This could introduce namespace conflicts, but those conflicts would be defined by the creator of the type, and would be no greater than if the methods had been implemented directly.

Does this sound like something that others would like to see implemented? It seems like it could be really ergonomic, and could seriously reduce the pain that arises from using fine-grained traits and needing lots of use statements.


#6

Totally agree with this. In principle there’s simply no other way to improve the situation OP described without breaking the valuable guarantees that the existing rules are there to uphold.

I could’ve sworn someone’s proposed this in the past but I can’t locate an RFC or an internals thread on it. I can find https://internals.rust-lang.org/t/pre-rfc-inherent-methods-from-any-crate/3821 and https://github.com/rust-lang/rfcs/issues/1971 which address two fairly similar issues, but definitely not this exact issue. I suspect the really interesting question here is whether we can come up with a concrete proposal that solves more than one of these annoying corner cases.


#7

Right, @HadrienG. Name conflicts can always be an issue, whether the function/method is defined on a type’s own impl block, or on a trait’s impl for the type. So, I assume name conflicts is not an issue here.


#8

Thanks, @droundy and @Ixrec, for the insights. I agree with you, and think that this could be a good addition to the language, specially along the Abstraction without overhead roadmap goal. I like to hear how @nikomatsakis feels about this.

Well, kind of. I think for now we’re going to have the actual implementation in the type impl block, then have a inline shadow impl in the trait impl block to satisfy the contract.

Yes, that sounds reasonable, specially since Rust has dropped class-based inheritance in favor of trait-based sharing, and missing this feature means more cost for users when dealing with similar API design.

Another related issue I found is https://github.com/rust-lang/rfcs/issues/1531, which was for a slightly different problem.

I suppose one possible syntax for this would be:

pub impl MyContract for MyEnum {}

or

pub(extern) impl MyContract for MyEnum {}

meaning that this implementation is exposed publicly with the type itself.

On problem with this, though, would be conflict with the pub(crate) tag suggested in https://internals.rust-lang.org/t/pre-rfc-inherent-methods-from-any-crate/3821/3.


#9

Well if you don’t mind the redundancy you can define a foo method on the intrinsic impl as well. Just make sure it does precisely the same thing :stuck_out_tongue:


#10

I don’t want us to spend too much time discussing this overall minor point when you make a more important point about trait usability otherwise. However, there is an important difference between the way traits handle name conflicts with respect to raw methods.

If you need to implement two methods with the same name in a given impl block, you’re stuck. Period. Rust doesn’t allow this form of method overloading.

If you need two methods with the same name in two different trait impls, on the other hand, Rust gives you tools to work around this, such as the disambiguation syntax <Type as Trait>::method(receiver, args);, or careful scoping of use Trait statements.

In situations where you are sure that no name conflicts can occur, and don’t need this safety net, you can also use the trick mentioned by @Fylwind of implementing the methods in the struct’s impl and mirrorring them in the trait impl: https://play.rust-lang.org/?gist=e9913b2d77194aaafae76d29cd966bb5&version=stable .

This is a bit tedious and verbose, though, so if I had to do this a lot, I’d probably implement a macro to automate it, and request some form of integration into the core language if many people express a similar need.


#11

I think this could probably be covered under one current RFC: delegation of implementation. As an example I think the gist you linked may be able to implemented like this if that RFC is approved and implemented.


#12

@Nemo157, that’s exactly the kind of feature I’m hoping to have in the language. Thanks for the pointer!

In fact, looking at the RFC, I think this would be also possible, which is more like what I was asking for:

    impl AnswerToUniverseLifeAndEverything for MyAnswer {
        use MyAnswer;
    }

Which would delegate the whole trait implementation to the type.