Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Delegating permissions to implement Trait #2946

Open
Licenser opened this issue Jun 19, 2020 · 8 comments
Open

Delegating permissions to implement Trait #2946

Licenser opened this issue Jun 19, 2020 · 8 comments

Comments

@Licenser
Copy link

Currently, it is only possible to implement traits on types if either:

a. a crate owns the type
b. a crate owns the trait

The reasoning for that, as far as I understand, is that allowing random crates to implement traits on types where they own neither we quickly end up in a situation where there are competing implementations of traits in which rust can't decide anymore which is the "right one".

This can lead to an unpleasant situation for traits that are generally useful where:

A crate implementing a common, useful, widely used, the trait has to depend on and implement traits on a large number of other crates they wish to implement the trait for - usually accompanied by a number of feature flags so users can cut down the dependency graph.

A crate implementing a common type having to depend on and pull in a number of trait-crates to implement their traits. Again usually accompanied by a number of feature flags to cut down the use of unused dependencies.

An example

The probably best example for such a case is serde, which, rightfully so, a lot of crates depend on because it offers an important trait that crates might implement.

At the time of writing, there are over 5000 crates (https://crates.io/crates/serde-json/reverse_dependencies) depending on it which to a large degree, if you just use them for functionality do not require serde at all.

(note this is not a criticism on serde or any library using its traits!)

Proposal

Allowing crates to delegate the right to implement traits on their types, or implement their trait for types to other crates would allow people to opt in / out of dependencies in an independent manner from feature flags.

In other words, a crate a could specify a list of other crates (b, c, d) that are allowed to either B, C, D are allowed to implement traits in A on types that are neither owned by A nor themselves or implement traits that neither they nor A own on types owned by A.

Since permissions would need to be explicitly delegated by a create that has the permissions themselves this would not break the initial reason of conflicting trait implementations on types.

Cargo.toml (for crate a)

delegate-trait = ["a", "b", "c"] # might need a better name but that's a detail

An example (again):

The user of the url crate could delegate permissions to a crate url-serde so that url-serde could implement Serialize and Deserialize on the types defined in url. This would remove the need for future flags and people wishing to use serialization with url can explicitly add the extra dependency.

limitations

A limitation with this minimalistic approach would be that derive based traits couldn't be delegated. However, nothing in this proposal would block adding this later on with an additional construct around deriving for foreign structs.

@Ixrec
Copy link
Contributor

Ixrec commented Jun 19, 2020

See https://github.com/Ixrec/rust-orphan-rules for a more complete summary of why this stuff is the way it is and existing proposals haven't gained much traction.

This specific idea of the trait-owning crate allowlisting potential implementer crates usually runs into a "not solving the actual problem" objection like this one:

https://internals.rust-lang.org/t/revisit-orphan-rules/7795/59?u=ixrec

perhaps we could brainstorm some sort of whitelisting mechanism where diesel and chrono both declare that diesel-chrono has special permission to impl their items.

That kinda defeats the purpose. The idea is that someone should be able to make this crate without either of the parent crates having to know/care/get involved

@Lythenas
Copy link

Lythenas commented Jun 19, 2020

@lxrec was faster. But here is an example that illustrates why I think this would be hard to do.

I think this could work if you only delegate permissions to a single crate. But this kind of defeats the purpose. An IMO you can just as easily use feature flags for this (and might even be more flexible with feature flags).

If you allow multiple other crates to implement: What happens if there are actually multiple crates that do implement the trait for your types somewhere in your dependency hierarchy? For example because you have both b and c as a dependency.

This issue could probably be avoided for the direct dependencies of your crate, but it gets more complicated (or impossible) for indirect dependencies. E.g. for something like this dependency tree:

YourCrate
- Def1 (delegates permission to crates ImplA and ImplB)
- SomeDep1
  - Def1
  - ImplA
- SomeDep2
  - Def1
  - ImplB

In this scenario you wouldn't be able to use both SomeDep1 and SomeDep2 together because ImplA and ImplB aren't be allowed to be used together.

@Licenser
Copy link
Author

This issue could probably be avoided for the direct dependencies of your crate, but it gets more complicated (or impossible) for indirect dependencies. E.g. for something like this dependency tree:

The exact same issue exists today with feature flagged implementation of traits. If you have a the-type create and a the-trait crate where that feature flag each other for implementations of the trait and you have two indirect dependants that include the respective feature flage.

I would go so far that this is "worse" then what would be introduced with this proposal as the implementer of the-type and the-trait do not ever need to have communicated while the owner of Def1 and ImplA and ImplB will need to have communicated (and I suspect but that's just a suspicion) will mostly be the same person.

@burdges
Copy link

burdges commented Jun 19, 2020

I'd think the serde case should always be handled by optional dependencies, but optional dependencies could be improved dramatically. We could make it far easier use dependencies only when another crate requires them.

[dependencies]
serde = { version = 1.0^, optional = implicit }

I think delegation proposals envision crate authors creating micro subcrates, but doing this seems far less user friendly than the current optional dependencies scheme, not only for the crate developer, but also downstream. In short, the serde example gives a reason why rust should not do this even if it could do so.

@Ixrec
Copy link
Contributor

Ixrec commented Jun 19, 2020

Note that delegation and optional dependencies/automatic features are targeting different kinds of orphan rule pain. AFAIK there's a rough consensus that we probably would like both in some form, but finalizing either of them is not a priority for now.

(and as usual, we're just recapping points made a million times in previous threads about this)

@Licenser
Copy link
Author

I think delegation proposals envision crate authors creating micro subcrates, but doing this seems far less user friendly than the current optional dependencies scheme, not only for the crate developer but also downstream. In short, the serde example gives a reason why rust should not do this even if it could do so.

I'm curious how you come to the assessment that it's less user friendly. feature flags are entirely opaque, often the only way to find them is to inspect the Cargo.toml of a crate. Removing default features is problematic, you got to remove all and then add the ones back that you want then hope that in the future the defaults don't change and break things. Removing partial flags that are pulled in by implication is also not possible.

On the other hand, dependencies are a much more elaborate system with more capabilities, they're easily explorable, they're (mostly) independent of each other by design, they allow overwriting via the cargo toml if needed.

There is an additional burden on releasing more then one new version if a trait fundamentally changes to the level of breaking backwards compatibility.

@Ixrec I might be missing something but the discussion you linked was about allowing "random" traits to implement Features on Types, there is a very distinct difference in doing that and allowing explicit delegation of permissions. It's erveryone-has-root vs. there-is-a-sudoer-group. But I probably might miss other discussions on the topic?

@Ixrec
Copy link
Contributor

Ixrec commented Jun 19, 2020

The repo and the IRLO thread I linked both cover a lot of ground (especially if you follow all of the other stuff they link to), including forms of allowlisting as proposing here, abandoning the orphan rules to allow "random" impls, and just about everything else in between.

@ssokolow
Copy link

ssokolow commented Oct 9, 2021

I'm curious how you come to the assessment that it's less user friendly. feature flags are entirely opaque, often the only way to find them is to inspect the Cargo.toml of a crate. Removing default features is problematic, you got to remove all and then add the ones back that you want then hope that in the future the defaults don't change and break things. Removing partial flags that are pulled in by implication is also not possible.

Given that you appear to have opened this in response to a tweet about the discoverability and related UX elements of features being terrible, I think it'd be more productive to discuss the root problem (features UX), rather than just taking for granted that this is the best solution.

...and yes, I do agree that Cargo's support for features has terrible UX which encourages building with features you don't need because it makes it so laborious to learn of the existence of and turn off the ones you don't need.

Maybe at least something on crates.io, lib.rs, and built into cargo itself that functions similarly to cargo-feature-set... a command, might I add, that I only just learned of now from by accident despite actively searching for something like it multiple times over the last three years... though I still need to open a feature request for an option to have cargo-feature-set hide transitive dependencies I can't affect. (EDIT: Done.)

Maybe also some kind of negative feature support in Cargo.toml so you could do something like this:

[dependencies.foo]
features = ["-bar"] # ...but keep baz

...instead of something like this:

[dependencies.foo]
default-features = false  # Disable bar
features = ["baz"]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants