-
Notifications
You must be signed in to change notification settings - Fork 17
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
Ascent should omit trait bounds on the generated program's type declaration #23
Comments
Hmm, do you have a concrete use case where this is an issue? I don't think the guideline applies here, for a number of reasons:
Again, if you have a concrete and realistic use case where this becomes an issue, please share it here, so I can take a closer look. |
Hiding the program as an implementation detail within another generic type, without leaking its trait bounds. tl;dr: The trait bounds on a type's declaration deal with its structure. The trait bounds on a type's impls deal with its behavior. And having those two separated was very conscious decision in Rust: Just because parts of a type's behavior (even if the part is actually its whole behavior) require a certain trait you shouldn't be blocked from adding it to another type.
Yes, that is a fair point, but more a limitation of Preferably there should be a way for the macro body to declare where to add the trait bounds to, for sure. One possible approach might be to allow this: ascent!{
struct AscentProgram<T> where T: Clone + Eq + Hash;
// ...
} but also this: ascent!{
struct AscentProgram<T>;
impl<T> AscentProgram<T> where T: Clone + Eq + Hash;
// ...
} If you think this Update: support for aforementioned syntax has now been implemented on the PR: #25 (comment)
The same can be said about pub struct HashMap<K, V, S = RandomState> { /* private fields */ } And only those methods that actually require impl<K, V, S> HashMap<K, V, S>
where
K: Eq + Hash,
S: BuildHasher,
{ … } Everything else just is impl<K, V, S> HashMap<K, V, S> { … } Type Pretty much the only times where it is required and preferable to put trait bounds on a type's declaration itself is when those traits deal with allocation. As such pub struct Vec<T, A = Global>
where
A: Allocator,
{ /* private fields */ } And pub struct BTreeMap<K, V, A = Global>
where
A: Allocator + Clone,
{ /* private fields */ } It's only the state/allocation-relevant traits that are bound on the type's declaration. Everything else gets bound either on a per-impl, or even per-fn level: impl<K, V, A> Clone for BTreeMap<K, V, A>
where
K: Clone,
V: Clone,
A: Allocator + Clone,
{ … }
impl<K, V, A> BTreeMap<K, V, A>
where
A: Allocator + Clone,
pub fn get<Q>(&self, key: &Q) -> Option<&V>
where
K: Borrow<Q> + Ord,
Q: Ord + ?Sized,
{ … }
// ...
}
Yes. And that's exactly why the bound should be omitted on the type's declaration: Your outer container types should not have to leak their field's bounds.
Imagine a scenario where a generic type has multiple fields, one of which bing an ascent program: pub struct Foo<T>
where
T: Clone + Eq + Hash
{
// ...
program: AscentProgram<T>,
}
impl Foo<T>
where
T: Clone + Eq + Hash
{
pub fn do_the_ascent_thing(&mut self) { ... }
pub fn do_something_else_entirely(&mut self) { ... }
pub fn do_another_unrelated_thing(&mut self) { ... }
// ...
} The However due to If however pub struct Foo<T> {
// ...
program: AscentProgram<T>,
}
impl Foo<T>
where
T: Clone + Eq + Hash
{
pub fn do_the_ascent_thing(&mut self) { ... }
}
impl Foo<T> {
pub fn do_something_else_entirely(&mut self) { ... }
pub fn do_another_unrelated_thing(&mut self) { ... }
// ...
} We're now free to both create Here is an in-depth discussion on the topic: https://stackoverflow.com/a/66369912/227536 |
I definitely prefer this approach to shifting the user's defined generic bounds to the impl block. You mentioned a scenario where you have a field of type |
I was trying to wrap the As another hypothetical scenario you may not be able to defer the creation of the program to the immediately before running, but might have distinct setup and run phases (and prefer not to introduce an intermediary type for holding the program's "ingredients" instead of itself). But either way behavioral trait bounds should never be added to the type declaration as outlined above. They provide zero benefits, but lots of problems. The |
Even assuming I agree with this completely (I'm not sure I do. Trait bounds on type definitions clearly signal the intended use to the user), the question is whether it's worth the added complexity to have different trait bounds on type declarations and impl blocks on Ascent. When you are defining a generic type directly, it takes 0 added complexity to not put trait bounds on the type definition. Anyway, since you've already done the work, I don't have issues with it being added to Ascent, and thanks for the PR! |
For generic programs ascent currently requires something along the following:
or
which then generates code that looks something like this:
This unfortunately tends to lead to problems with rather nasty effects one one's code and is thus generally considered an anti-pattern.
As such the official Rust API guidelines has a chapter on the topic (it talks specifically about derived trait bounds, but the viral breaking change effect is not limited to derived traits):
In short the problem is that trait bounds attached to a type's declaration spread virally (i.e. they require your surrounding code to now also be bounded by these traits), while those only attached to a type's implementation generally don't.
As such the generated code should preferably look like this instead:
This change should have no affect on ascent itself, but make integrating it much more pleasant and less intrusive.
The text was updated successfully, but these errors were encountered: