[lang-team-minutes] Const generics

I want to set expectations realistically here: to stabilize we have an FCP which is usually 6 weeks long, then the feature is in the beta branch, then 6 weeks later its stable. There's a release on January 4, 2018. Targeting stability in that release, we would need to have decided we want it to be stable by October 12, 2017, less than six months from today.

That would give us 6 months to finish the RFC process, do the implementation, get experience using it, make sure the implementation is solid & not buggy, be comfortable that it is ergonomic and comprehensible, discover edge cases, decide what to do about them, and so on. Realistically that cannot happen for a feature as large as const generics.

It will likely be will be implemented on nightly by some time in fall, but I would expect several quarters from then until the feature is ready to stabilize.

6 Likes

Yes; if equality isn't reflexive, though, you could never call a method on a Foo<NaN>, because it would not be the same type as the Foo<NaN> the method is defined on.

In general its better to think in terms of structural equality (which floats don't have defined today). That is, would if let NaN = NaN { } enter the conditional? Its not obvious either way, I think. Of course I agree with other posts that this can be dealt with later and separately from const generics.

Itā€™s worse than that! You can encode non-deterministic computation (or dependent on the host compiler) using float intermediaries, but with only integer inputs and outputs.

The plan to deal with that, in the long term, is to use something like LLVMā€™s APFloat, either through bindings (@edef has had some success there) or wholly converting it to Rust, which implements IEEE 754 semantics in an arbitrary but host-independent deterministic manner (for a given specific version of the compiler).

Anything less can end up with trivial unsoundness, which we should really avoid. See the related Idris issue (that was prompted by a #rust-offtopic discussion more than a year ago).

What we can, even without a better way to evaluate floating-point operations, is to ban floating-point expressions that appear in a type (e.g. [T; float as usize]) which also refers to const parameters or associated consts/const fn that are type-parametrized (e.g. [T; (C as f64 / 0.0) as usize] or [U; C + (0.0 / 0.0) as usize]).

The reason behind that is that a flawed floating-point computation cannot create unsoundness if it happens only once and you always cache the result. This can be done for all constant expressions, except those that need any parameters in order to be evaluated.

Nothing like that can successfully end up in a type in current Rust, even on nightly, so we wouldnā€™t be breaking any code if we introduce that limitation.

5 Likes

What makes this easier for me (I know this is a common pattern) is to give the params consistent names across different impls and types, so R is always the type which implements Resource & S always implements Service and so on. My experience is that once I know "which type" <T, Q, R, S> are all, I can understand the signature pretty okay without referring back. (Of course implied bounds also helps here in making each impl primarily list the new bounds it adds.)

Reading things that way, I think having some notation for 'threshing out' the consts and types is helpful. <T, Q, R, S; N, M> just seems mildly more readable to me than <T, Q, R, S, N, M>. But this does seem like YMMV, and I think other people (validly) interpret the ; in connection to its semantics in other contexts & other languages.

FYI: The questions about floats here seem related to the question of whether to allow matching on floats. That was originally allowed, but is currently being deprecated:

I understand the reason why it seems silly to use ; to separate the type and const generics. That being said, I do like the idea of being able to write syntax like:

struct S<T, U=i32; V: usize, W: usize = 10> { ... }

impl<T; V: usize> S<T; V> { ... }

It seems elegant to

a) Allow for const parameters to easily at all places where they are defined or used be separated from type parameters and lifetime parameters b) Follow the same syntax as [u8; 10] (although I donā€™t really care about that that much) c) Allow both optional const and type parameters separately.

Iā€™m not super picky though, although I donā€™t like the idea that you wouldnā€™t be able to have a optional type parameter if you have a const parameter, which enforcing the order without ; would require.

5 Likes

I think what would mitigate the ā€˜kind apparentnessā€™ issue is not only having a SHOUTY_SNAKE_CASE style preference, but also a strong style preference for making const params actual words, instead of single character identifiers. I think this is a good idea regardless, since const params will be fewer in number and more narrowly ā€˜meaningfulā€™ in the sense that they can operate at the value level.

For example, we should be talking about [T; LEN] instead of [T; N]. Given a BindClient<T, KIND> its sort of obvious that T is a type and KIND is a const if people abide this style recommendation.

I still kind of want to have the semicolons if we do this, but I can accept not having them if thatā€™s the majority preference.

@mystor We should be checking for omitted defaults in a context in which we know the kind (lifetime, type, or const) of each parameter; for that reason we can always figure out which types are missing instead of just which params (of any kind) are missing.

3 Likes

This is not really a problem if the convention is to have lower case name for consts. I don't think shouting snake case make sense for const parameter (I'm not sure it make sense for normal const anyway but that's another topic).

IMO const parameters are different from normal const : they are not as much 'constant' since they are 'parameters' : they vary for every use. The ; would solve the problem only in the declaration anyway. Since generics are usually one letter long, they would be easily mistaken for types at first sight in the implementation too. It make much more sense to me to visually see const parameters the same way as immutable variables.

3 Likes

Just as a data point, in Rayon I used to use words for type parameters (e.g., ITEM instead of T), but this was eventually removed. It's worth looking over the PR just to get a feeling for what generic code with UPPERCASE_WORDS looks like. (I have mixed feelings about changing to single letters; but I'll note that I did try SnakeCase as well and I found that actively confusing, since it was hard to distinguish type parameters from actual types.)

1 Like

My first impression from looking through the diff - huge regression in readability.

Maybe it's not so noticeable if you are the code author and know it by heart, but these kind of things are very important when you see the code first time in you life and need to fix it urgently (for some reason this happens with me all the time at work :().

(ALL_CAPS types were strange though, I'd use SnakeCase as usual for types.)

3 Likes

I agree with you that lower-case for constants would make distinguishing types and constants a non-issue.

However, it would make using existing const values, which have SHOUTY_SNAKE_CASE, inconsistent.

The problem is not so much the declaration site as it is the use site: in the use site you have to use an existing constant, and constants today are SHOUTY_SNAKE_CASE.

Iā€™d be in favor of moving away from this constant style toward regular snake_case (I really donā€™t see the point of distinguishing them so), but that should be a separate RFC, and seems pretty annoying at this point.

2 Likes

Iā€™m also weakly in favor of not using SHOUTY_SNAKE_CASE for constants, mostly because in my C++ world shouty snake case is not used for constants, itā€™s used for macros. To me that usage is as much a warning as it is a disambiguator, and neither Rust macros nor const generics are features that inherently require a warning the way C++ macros are. For me actual constants have always been camel case and I canā€™t recall it causing any ambiguity problems. While I do use all caps for my type parameters, my type parameters are usually 1-3 letters and never multiple words, so it doesnā€™t really feel like shouty snake case.

I was on the fence about semicolon-as-kind-separator for a while, but I think Iā€™m convinced now that Iā€™d rather not imbue the semicolon with that special meaning. It doesnā€™t even work in the single parameter case, thereā€™s a risk of not composing well/being consistent with other kinds, it canā€™t do nearly as much to disambiguate which parameters are which as good names and a standard style could (both of which are probably mandatory with or without the semicolon rule), I donā€™t see a need to forbid parameter lists like Foo<T1, LEN1, T2, LEN2>, and perhaps most importantly, it feels largely orthogonal to everything else being proposed (unlike the curly braces syntax).

5 Likes

Yeah I agree. If anything deserves to be SHOUTEY_SNAKEY, itā€™s static muts and statics containing some flavor of *Cell: i.e., global mutable variables. Immutable statics and consts seem totally benign with no reason to attract attention to themselves by loudness. (FWIW, I expressed the same opinion back when the convention was adopted.)

This is somewhat confounded by modules and lifetimes being lowercase, but one convention that might make sense is if CamelCase were for ā€œcompile-time thingsā€. That would mean normal consts, associated consts, as well as const parameters would be CamelCase - just like types - while immutable statics would be snake_case.

7 Likes

Yeah, these are exactly the thoughts I had in mind back when I first proposed this syntax. (Together with things like type<type> F for higher-kinded types, if/when we get them, and maybe [type] Ts for variadic ones, so that all kinds have a consistent prefix notation.)

I'm not sure it would be worth all of the communicational friction to actually change the lifetime syntax at this point, though? (To be honest that possibility hadn't even occurred to me.) It seems like quite a large amount of pain for a rather smallish gain.

One thing I feel this thread is lacking is a good/great reason for adding this new syntax to support this feature. The feature itself sounds great, but what benefit does the additional syntax provide? i.e. impl<T; const N: usize> vs impl<T, N: usize>

It seems like this additional syntax const is just unneeded baggage surrounding this feature. If the generic param is a "sized type", then it is very intuitive to know we need to supply a const value of that type. Additionally, if it is not as intuitive as I think, it seems a case where we can have really good error messages and in general docs.

Tying my argument to the ergonomics inititive:

Applicability. Where are you allowed to elide implied information? Is there any heads-up that this might be happening?

While not the most frequently used feature, it will still be used often enough to add unneeded friction while learning but especially as an experienced developer.

Power. What influence does the elided information have? Can it radically change program behavior or its types?

The proposed syntax has zero influence on program behavior. Regardless of the additional syntax, if the user of this particular generic puts in the wrong value, there would be a compile time error with hopefully good error messages.

Context-dependence. How much of do you have to know about the rest of the code to know what is being implied, i.e. how elided details will be filled in? Is there always a clear place to look?

Regardless of the additional syntax, the user needs to know the signature of the generic they are trying to use, and likely already has its rustdoc open. The only additional context is that when a "sized type" is a generic parameter it requires a const value during use. While arguably intuitive, this is a very simple rule which only needs to be learned once.

Finally, if a developer has a personal preference to have this distinction in their code: They are not restricted from using comments to add this distinction to their source files. eg. impl<T, /* const */ N: usize>

1 Like

For one thing, impl<Foo: SomeTrait> could mean either a type parameter bounded on SomeTrait, or a const parameter whose type is a trait object for SomeTrait. Not that the latter is especially useful...

2 Likes

More importantly, types and constants need to be distinguished at name resolution, before type checking (which relies on data from name resolution) can be performed. You need type check to run before you can tell the difference between a sized type and an unsized type.

3 Likes

They are all const parameters, just of different kinds (types, values, lifetimes).

How about another sigil? :grin:

Foo<T=u32, #V=42, 'a>

Foo<T: Bar, #V: u32>
1 Like

Iā€™m on a plane on mobile about to take off, but I wanted to add a request for this feature. I would hope it is possible to abstract over const and non-const functions / data structures. Specifically I wouldnā€™t have to write two versions of a function, one for const parameters and one for normal function parameters (non-const parameters).

I had this thought after reading @yazaddaruvala comment above.

Wouldnā€™t that need be supported by const fn or some other form of CTFE?

1 Like