Revisit Orphan Rules

I'm not sure, I'd have to go back and inspect what I did.

Does Rust support reflection on external structs using macros during compile time? As I see it, that would be another way to handle this, although it feels like a bit of a kludge.

1 Like

If by external, you mean a struct defined in a different crate, then I believe the answer is no (please someone correct me if there is something I'm not aware of). There is an in-development run-time reflection API, rust-reflect. That being said, using run-time reflection to accomplish this goal, though imposing run-time overhead, might be the way to go for some of these kinds of things, but, it seems like a poor solution to the problem as well.

Perhaps, if there were a compile-time reflection API for structs that could be leveraged by procedure and/or rule-based macros, then many of these problems mentioned could be solved relatively painlessly. For example, if the answer is "New Type" wrapping to most of these problems, but, that is deemed too inconvenient/difficult to manage, then, could compile-time reflection coupled with appropriate macros provide the needed simplicity?

For example, in the Serde "Remote Derive" case, if Serde could define a macro that utilized compile-time reflection to create new-type wrapped remote- serde implementations for any 3rd party struct, would that resolve that case? Would other cases be amendable to a similar solution? NOTE: When I say "Compile-Time Reflection" I mean that there would be an annotation that could be placed on a struct (in the crate that defines the struct) that would cause compile-time reflection information to be generated and stored in the compiled lib. Then, macros, could have an API to access the stored compile-time reflection information about any struct. So, you could do something like

use SomeCrate::SomeModule::SomeStructThatHasReflection;

[#serde-remote-reflect]
type MySomeStructThatHasReflection : SomeTypeThatHasReflection;

To get a full "New Type" wrapped Serde Remote implementation for SomeTypeThatHasReflection provide it was annotated with the compile-time reflection annotation and that it met the rules that Serde would demand for auto-derive, like say, the struct must have no private members.

Maybe something like this, Reflect should be explored with respect to this problem space.

1 Like
  1. I think that if we can find a nice way to have a “DieselChrono” crate without requiring Diesel to depend on Chrono or vice-versa, we’ll do a good amount on reducing the problems that come from the absence of coherence in practice.

    If such a crate can be allowed without Breaking The World terribly, I believe it will solve a real need in the ecosystem.

  2. With specialization and Rust’s separate compilation model, breaking the orphan rules can have terribly confusing and very unsound (if associated type specialization is used) results, because a parent crate can observe an impl not existing while the child crate observes it existing.

    This should be mitigated when we get specialization_predicate to cut down on negative reasoning, as long as specialization predicates can’t be orphans.

  3. For the “deriving Serialize on structs I don’t own” usecase, what you want is not being able to insert orphan impls, but rather to be able to reflect on fields of structs you don’t own.

    First, being able to impl Serialize yourself won’t be much help if you need to write the serialization code yourself. Second, if you can auto-derive, you don’t quite need to inject an orphan impl, but rather you can write your serde functions as free functions.

If you have enough information to Serialize an external struct that isn’t Serialize, then you can use derive. Create a new type holding just that information, implement conversions, use the derive on it, and then use serde’s remote serialization feature.

Serialize is not motivation for this, as serde has a fully functional way of handling this, using the derive, through remote derive. Diesel/chrono is a much better example of where loosening the rules might help.

As a potential idea: what if you were allowed to declare an interop crate that was allowed to implement traits of each of two crates for the other. There would need to be some way to restrict it to one interop per compilation though. (We already have the issue with -sys crates though (of can’t have duplicate crates).)

2 Likes

I really don't understand why people seem satisfied with the additional performance overhead that this imposes.

1 Like

Why do you think there’s a performance overhead to any of these suggested newtypes?

I mentioned something like this in an incredibly spit-bally fashion here: Moving bits of rustc into crates - #35 by gbutler. I guess I could envision something like:

  • Declare "Friends" Crates in the Cargo.toml
  • Declare modules with "friend" visibility to "friend" crates in the main.rs/lib.rs
  • Possibly have a "friend" visibility specifier for items to allow items that are private to a module/struct to be visible to "friends" of the module??? Otherwise, private items become effectively public to friends if not.

This probably requires more thought. Obviously, this would change the compiler as symbols that were private to the crate (or those with "friend" visibility specifier) would now have to be exposed to "friends" but not "public".

Cargo and/or the compiler could possibly be taught to give warnings when changing "friend" visible items. Perhaps, Semver rules could be extended to say that any change to a "friend" visible item would require a minor version bump and that would be a breaking change for "friend" crates, but, not non-friend crates (or something like that).

I thought some more about my last example, and I talked to a Haskell programmer about it. The central problem – converting a Container<T> to a Container<U> when you have a type T that is convertible to U – turns out to be exactly the use case for Coercible. I did some searching and found an old Rust RFC about this: https://github.com/rust-lang/rfcs/pull/91

Unfortunately, it hasn’t seen much attention since 2014.

Coercible seems like an ideal solution to a bunch of the inconveniences of the newtype pattern. With both delegation and Coercible, I think I’d find the newtype pattern convenient enough to use for this situation, reducing the need to relax the orphan rules.

2 Likes

A couple things just occurred to me:

  1. On one hand, Coercible potentially implements my idea of “a syntax for making blanket impls for structs with public fields” – if a struct with all public fields was Coercible into a tuple, then you could blanket-impl a trait for all types Coercible into tuples. Although maybe this would lead to too many overlapping impls – I haven’t thought out the details fully. Then you wouldn’t even need to explicitly use delegation in the newtype pattern, although…

  2. On the other hand, for some traits, such as Ord, there are multiple reasonable implementations for the same type, and you might want to make a newtype that implements the same trait in a different way, or even doesn’t implement the trait at all. In conclusion, it seems important to make the newtype pattern convenient enough (through Coercible and delegation) that you can get away with the struct not already having an implementation of the trait. The “coerce into a tuple” idea is still useful this way, too, because when you’re making your newtype, it would let you delegate to the tuple implementation.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.