Revisit Orphan Rules

Can’t you already do this? That’s what I thought Little Orphan Impl was all about, opening up the orphan rules a bit so that you could do this as long as a “covered” type was locally defined.

(Obviously, in general, you wind up with exactly the same overlapping impl issues.)

The newtype pattern seems like it technically works fine, it just has loads of boilerplate.

It's even easier than that – all the examples I could come up with had a very specific thing in common. If there was a standard trait expressing what they had in common – call it IterableOwningContainer<T> – all of them could implement it. And then the same library that provides the VisitOwnedWeirdStructs trait could provide a blanket impl for all types that implement IterableOwningContainer. That wouldn't even need a language feature. (Except for generic associated types, because you can't make the iter() function be a trait method without them.)

Then there's the case of structs and enums with all public fields/variants. I'm not sure that could be covered with a standard trait. Maybe there could be a language feature allowing a library to implement its trait for all such structs, kind of like how libraries generally implement their traits for all tuples (up to a certain size, but that restriction is only for practical reasons)

Isn't that how blanket impl's already work? The creator of a new trait can provide blanket impl's of that trait for a structs (past/present/future) that have impl's provided for some specific set of other traits.

1 Like

Yes, exactly. That’s what I’m saying. The only thing we’d need to make this work is an appropriate trait over which to do the blanket impl.

It seems like this is looking for some sort of conditional compilation based on optional dependencies, i.e. having a module which only gets compiled if a given optional dependency is available. You could come up with a way delegate implementation to a third crate, but that seems unnecessary. Having conformance of a type in crate A to a trait in crate B depend on importing crate C seems like a potential stumbling block.

On the other hand, the third-crate approach means that you could potentially distribute libraries in binary form without any additional complications. The third-crate is just another library you need if you want to use it. With optional dependencies, a library would need to be compiled against all its optional dependencies in order to be distributable in binary form. You'd then be relying on conditional linking to prevent the conditional modules from being included in a final executable.

No. My point is and has always been that neither Diesel or Chrono should have to care about each other. The ecosystem should be able to integrate those two crates without having to be blessed by the authors of those crates.

1 Like

Are you saying you’d rather drop the coherence guarantee completely so that crates like this can be written without blessing?

No

1 Like

Okay, so if the idea is to keep orphan rules but allow a true third-party to provide arbitrary implementations, then the question is how to restrict this to minimize potential for conflicts. One option was to allow certain traits to opt out of the orphan rules, but it seems unclear from here when a trait should decide to do this. And your true third-party can’t exist without this annotation, so we’re back to crates having to bless extensions, at least in a general sense if not individually.

Another angle would be to only allow arbitrary impls in special extension crates which can only be imported in the root crate (like a crate for an executable, but making allowances for testing as well). Such crates could be forbidden from having public members to ensure there’s no secondary reason to import one vs another, minimizing the likelihood of someone wanting to depend on two incompatible extensions.

Crates that want to depend on an implementation being present in the final executable (other than the final executable) would have to use something equivalent to a where clause, essentially saying a particular function, impl, etc… or perhaps crate as a whole is contingent on that impl existing, which would then propagate to any context using it up until the final executable, which has to provide the implementation.

This would probably still be problematic due to blanket impls and such, which can result in a particular combination of impls being incompatible. This can happen today in a single crate, but it’s limited in that that crate won’t compile with the incompatibility. Under this kind of system, whole libraries/ecosystems could be successfully compiled using incompatible constraints, and you’d only get an error once you tried to combine them, at which point it would be irreconcilable.

1 Like

What of the subtyping sort of solution I mentioned above?

Could you clarify what disadvantages exist beyond inconvenient/tedious?

First of all, I'm not sure why inconvenient/tedious is seen as less of a reason. Making code easier to read and write is one of the primary functions a programming language satisfies, and the newtype pattern adds unnecessary overhead for both.

Regardless, here is my post outlining issues which, even if you were handed a completed newtype pattern, are still disadvantages to using it:

I think we're repeating ourselves now. I asked this same question earlier in the thread, and @mboratko pointed out things like code duplication making the newtype impl brittle, and my response was that I believe all those issues would also be solved by delegation, which is why I'd been lumping them together under "tedious boilerplate".

Assuming he's coming from the same position that I am, it's not "less of a reason" to change something, but it is a reason to think that the problem is not that the orphan rules exist, but that newtypes are too tedious to write and we need to figure out delegation.

6 Likes

Here I think that the word "unnecessary" is your opinion and not at all clearly a fact. I again say, that what you are asking for is the equivalent of, "I have a class in C++ (or some other OO language) and I want to change the implementation of its base-class and have everything else use it as if it were the original without issue."

I just don't think that is a reasonable stance and I've seen nothing in all this commentary to convince me that it is a reasonable stance. So, the "unnecessary boiler-plate" you mention is entirely necessary glue-code to basically enhance a 3rd party class in a coherent fashion.

2 Likes

Don't get me wrong, I think delegation would be great, but what about mboratko's issue 4? Since nobody posted example code before, how about this:

//In crate foo
pub trait Trait {…}
pub fn do_something <T: Trait> (values: &[T]) {…}

//In crate bar
pub struct Struct {…}
pub fn get_values()->Vec<Struct> {…}

//In my crate
struct WrapperStruct (bar::Struct);
impl foo::Trait for WrapperStruct {…}
impl everything else for WrapperStruct by delegation;
let values = bar::get_values();
// now how do I do_something on values?

//compile error, structs haven't been wrapped
do_something (& values);

//Runtime overhead
do_something (& values.into_iter().map (| value | WrapperStruct (value)).collect::<Vec<_>>());

(Or maybe Vec in particular has some trick that allows it to optimize that series of operations down to nothing, but that's not the point – clearly this issue would exist in many other cases.)

an example of an unimportant trate that would be great if everyone had but is not worth all the PR’s is https://github.com/servo/heapsize

Okay, but that’s a clear-cut example of a trait that can’t be implemented legitimately by third parties.

It can in the case of structs which have only public fields, no? (This is the situation I was in.)

Ah, true.

I think the best language feature to address THAT situation would be a way to make blanket impls for structs with all public fields, similar to how you can currently make blanket impls for all tuples.

1 Like

Or, restating this in terms of @mboratko's problem, we may be able to minimize the inconvenience using delegation, an RFC already under review. While that RFC does not solve the problem completely, it does reduce the scope of the problem to two cases:

  • #[derive(...)] doesn't work on newtypes unless the internal type also implements the trait, and
  • changes to public fields can only be reliably discovered via code inspection.

As others have pointed out, the #[derive(...)] case is particular to crates like serde, which are relatively rare. Further, as noted by @withoutboats, changing the orphan rules to allow preferred impls will require recompilation, and wouldn't solve the derive problem, because that would break type safety.

The other case, as I see it, comes with the territory. It may be worth considering whether a tool, such as a cargo plugin, could detect these changes, but I don't see a way to build a feature into Rust without running into variations of the derive problem.

It's worth noting that both of these languages use runtimes that support reflection. This allows them to have more information about their types than Rust, which often eliminates types during compilation. Did you use reflection, or a feature that depends on it, to implement your project in Python or Go?