Feature request: where-as bounds

say we have a complex struct Foo

struct Foo<'a, 'b, ..., A, B, C, D, ...> {
...
}

with its impls:

impl<'a, 'b, ..., A, B, C, D, ...> Foo<'a, 'b, ..., A, B, C, D, ...> where
A: Clone,
B: Debug,
...
{
fn bar(&'a self) { ... }
...
}

now after some heavy development we may see this impl become insanely big, and make us want to split the methods to separate module, say, sub-module impl_bar:

mod impl_bar;

now, remember that the generic bounds of Foo is really complex, and we have to repeat these bounds in our new file impl_bar.rs

How about we create a new syntax, say we can write this in impl_bar.rs

fn bar_helper<'a, 'b, ..., A, B, C, D, ...>(&'a Foo) where as super::Foo { ... }

so we don't have to copy bounds from struct Foo to write our helper function

in detail we can copy token tree from bounds of Foo, and check if generic parameters are compatible

with this feature we will encourage user to wirte more sane code

For the specific case of bounds which are present on the struct definition, it's planned that bounds would be implied by default and not be needed to be repeated. Search for "implied bounds."

But also... don't write structs that are that terrifyingly generic.

10 Likes

Would matching happen by name or by position? If I do:

struct Foo<'a, 'b, A, B> {
    // …
}

fn bar_helper<'b, 'a, A, B>(&'b Foo) where as super::Foo {
    // …
}

what is actually happening?

generic parameters generally identify themselfs by their index, so 'a from Foo should be the same as 'b from bar_helper.

but I would prefer that user have to write exactly the same name with the same order to avoid confusion if you need more generic parameters, you write it afterwards, like

struct Foo<'a, 'b, A, B> where &'a &'b A: Deref<Target=B> { ... }
fn bar_helper<'a, 'b, 'c, A, B, C>(&'a Foo, arg: &'b (&'c A, B, C))
where as super::Foo, C: Iterator<Item=&'a Foo>
{ ... }

No, it's the opposite. If you make it easy to write horribly complicated code, then that's what (more) people will do.

Also, you didn't really explain what this should do and how it should work.

3 Likes

Note that this means that the names of generic parameters and lifetimes are now part of a stability guarantee. That is a high bar to clear in my opinion.

4 Likes

The vague concept that's been hanging around in my head for fixing the repetitive generics problem was to add generics support to entire modules, similar to how D does it

Here the parameters and bounds are only declared once, on the impl module (syntax handwaving here), and everything inside has access, since semantically the entire module body is duplicated and parameters replaced.

Assuming a reasonable syntax, would that feel ok for everyone? Or anyone?

2 Likes

Generic modules as an idea has been tossed around before, and has generally met a weakly positive response. The semantics are fairly obvious (it's as-if each contained item has the same generics applied to it) and non-controversial, so it's mostly a matter of justifying the extra implementation work to support them.

The main complexity comes from graceful error handling for two main things:

  • Items which currently cannot be generic, such as statics or consts; and
  • impl blocks which are insufficiently constrained due to the module's generics.

There's also of course a number of other small questions that would need to be answered during implementation, but I think aren't really blockers to it. (The biggest remaining question I think is whether it would be possible to import module::<_>::Item and have a generic name in scope, and if so, if it's possible to specify the generics directly on it. Like APIT[1], though, I think that can be punted on by just disallowing it.)

[1]: argument position impl Trait

3 Likes

Thanks, I suspected as such!

The semantics of statics and consts should be the same regardless of whether they happen to transitively mention the generic type, so without a way to "escape" the generic module you'd have to simply declare them outside.

Not sure I get the insufficient bounds, if you didn't declare them you don't get them? Is it about can you impl Foo where ModuleT : Trait {}? I guess that makes sense, since you can do that on methods on generic impl blocks.

Getting a generic alias to the guts of a generic is a separate feature, even if at the moment that feature would only be useful for associated types, AFAICT.

Specifically, something like

struct Simple;

mod generic<T> {
    impl super::Simple {
        //^ ERROR the type parameter `T` is not constrained
    }
}

because desugared it'd be

struct Simple;

mod generic {
    impl<T> super::Simple {
        //^ ERROR the type parameter `T` is not constrained
    }
}
1 Like

Ah, I wasn't thinking about it in terms of desugaring. I'd keep going, but I'm sure this has been talked to death, I've not read up yet, and it's off topic.

For the specific case of bounds which are present on the struct definition, it's planned that bounds would be implied by default and not be needed to be repeated. Search for "implied bounds."

AFAIK this feature might not end up being implemented in its entirety, due to semver hazards.

Isn't such desugaring is rather limiting? We can say that module level generic just declares a type parameter, but items of it that will be generic should only be the ones that use the declared parameter.

This cuts off impls problem, and allows us to fix the issue with statics by just saying that they may not refer to these parameters.

This smells like a semver hazard to me where the content of some body changes its containing block's meaning. It feels to me like there should be a "this block does not care about the module parameters" marker to indicate intent (static can be one such marker).

It shouldn't - generic parameters are only added from module to its items and never vice versa.

Something like this:

mod foo<T> {

fn free_function() -> usize {
    std::mem::size_of::<T>()
}

}

Here, free_function depends on T based only on its contents. I think this is a hazard if free_function is not always affected by being in a generic module.

This is what I was referring to with:

So basically, yeah, either you should have to "escape" the generic module parameters, or just have to declare such items outside the module.

I see now. However, I was replying to a later comment:

So while we agree, there may be others that don't see such implications.

Sorry, yes, I was agreeing with you :smile: