Proposal: Consider structs with explicit local type parameters as local for impl


#1

Currently it is not allowed to impl on any type that is not declared inside the crate that contains the impl block. This is the correct thing to do because it prevents function name clashes or even worse trait impl conflicts.

However, I thick that this can safely be relaxed (I may be mistaken) to include impl on types that normally take some number of type parameters where at least one of the explicit parameters is a local type because that would require it to be only in the local crate.

Example

pub struct Foo(...);

impl Option<Foo> {
    ...
}

This would be now allowed. Normally rules about naming would apply, namely it couldn’t conflict with say unwrap.

Usage:

This would be especially helpful for implementing From/TryFrom for such a type.


#2

This would make it impossible to add methods backwards compatibly because they could conflict with a user’s “private” extension.


#3

Would it still work if these “private” extensions overroad the generic versions for the explicit types only and then output a warning that such hiding is happening. That should be able to cover the problem and then the code won’t change its meaning if there is a new version.


#4

Like most orphan rule loophole proposals, this only works in the basic case where the crate using the loophole is the leaf/application crate. If the loophole-using crate is itself a library depended on by other crates, then in principle you’ve reintroduced all the problems the orphan rules are supposed to prevent with complicated dependency conflicts and turning backwards-compatible changes into breaking ones. If we make the loophole only legal in leaf crates, then we’re creating a rather ad hoc distinction between what’s legal in leaf vs non-leaf crates and may accidentally discourage moving application code into reusable library crates. We could go on, but it’s basically the same flowchart of arguments as all the past threads on orphan rule exceptions.

I believe the current consensus is that we don’t want any mechanism that would allow someone to ignore the orphan rules entirely, but there are specific use cases which should be easier than they are today and can be addressed by other features, like delegation sugar, specialization or automatic features. See https://github.com/Ixrec/rust-orphan-rules for more details.


#5

If it overrides the original then it isnt coherent because the method will not be overriden in upstream crates.

This basic idea gets posted all the time and I don’t know why each person who posts it doesn’t imagine the many people who’ve already put time into coherence have thought of this.


#6

To answer your question, either because the proposers figure that someone hasn’t come up with it given that sometimes even obvious things are missed or because they think that the benefits gained from such an addition outweigh the compromises associated.

The second part is generally combined with not seeing eye to eye on the specific design goals.

Sebastian Malton


#7

Really, someone should just go make a proc-macro that generates the extension trait pattern.

#[ext(MyStrExt)] impl &str {
  fn blah() { .. }
}
// ..
trait MyStrExt {
  fn blah();
}
impl MyStrExt for str { 
  fn blah() { .. }
}

I always thought this pattern looks just a little derpy, but it has one major advantage: the extensions need to be explicitly imported with MyStrExt… certainly better than how in Kotlin, an extension fun String.blah(): Unit is imported as just “blah”.

I’m also left vaguely wanting a UFCS-like syntax like my_str.MyStrExt::blah()… I seem to recall @Centril wanting this?


#8

Oh yeah, very much so:


#9

I did read the thread about why the extension trait pattern was prefered. However, is it actually possible is proc macros to tell what traits a type has implemented? Because that is something that the language could just have instead.

Sebastian Malton


#10

This is not necessary for the transformation I have described.


#11

Right… I remember not being a huge fan of your proposal of allowing any random free fn to be used as a method due to potential resolution spookiness (imagine a dual to the Arc::clone problem). What I’m proposing is a production

expr := expr '.' (path '::')? ident '(' args ')' | ...

desugaring to

<_ as path>::ident(expr, args)

This desugar is meant to indicate that path must be resolvable as a single trait (so things like ptr.Box::leak(), slice.[T]::len(), and foo.(A+B)::a_method() are all forbidden).


Also, in the process of checking this is the desugar I want, I noticed that writing <_ as $type>::foo(bar); gives a diagnostic saying foo is not a method of $type, even if it is; I expected "$type is not a trait". Is this a bug?


#12

I have implemented this: https://crates.io/crates/easy-ext


#13

Sounds like


#14

Would it be possible to implement traits defined in the same crate as the type with type parameters if the type parameters all contain locals? Or do we run into the same case again?

Example:

struct TypeA;
struct TypeB;

impl From<Option<TypeA>> for Option<TypeB> {
    ...
}

I don’t think it should be every possible for this to conflict. Please correct me if I am wrong.


#15

It still could. Imagine if core had an impl like this one:

impl<T, U> From<Option<T>> for Option<U> where U: From<T> { ... }

and you had TypeB: From<TypeA>.


#16

Makes sense. I guess my desire to be able to do this stems from frustration from using NewTypes and how different they feel from actually using the interior types but still wanting to write using from and into and other core traits