Const generics and defaults

The Rust team has decided that const generics should be interchangeable with type parameters Const generics MVP hits beta! | Rust Blog

We actually think a more flexible approach would be to allow default arguments for const generics as well as for type parameters, in the current order. You see, types are types, and consts are consts, so the compiler is never gonna parse a type as a const or a const as a type. That is to say, you should just be able to have

fn foo<A, B = (), const X: usize, const Y: usize = 20>() {...}

and it just, work. because foo::<Type, CONST> is unambiguous, and foo::<Type, Type, CONST> would also work, and so would foo::<Type, CONST, CONST>. You still keep the rules of both "types come before consts" (and the user-friendliness that comes with) and "defaults come last", but you tweak the latter to be "defaults come last per kind".

5 Likes

Wouldn't there be issues with (), which is both a type and a value of that type, and unit structs?

You'd have to put those in {} or something, yeah.

But also hey we kinda already do something similar to this whole thing for lifetimes:

fn foo<'a, T>(_x: &'a T) {}

fn main() {
    foo::<String>(&String::from("foo"));
}

so why not extend it to types and consts?

Oh, also:

fn foo<const X: ()>() {
    
}
error: `()` is forbidden as the type of a const generic parameter
 --> src/lib.rs:1:17
  |
1 | fn foo<const X: ()>() {
  |                 ^^
  |
  = note: the only supported types are integers, `bool` and `char`
  = help: more complex types are supported with `#![feature(const_generics)]`

error: aborting due to previous error

Apart from (), this could also get more difficult to decide once const generics allow for user-defined types down the road.

OTOH, lifetimes have a leading sigil ', which makes distinction easy.

Personally, I think there will be cases where const parameter before type parameter will be a more natural ordering. In those cases arbitrary ordering would be a win. OTOH, a fixed, global ordering lifetimes-types-const would be easier to remember.

1 Like

Not only that, but it also makes things significantly more readable, at least for us. (When ppl show us C++ templates we have to spend a good few minutes trying to process the parameters... Granted, part of that comes down to all the token overhead it uses.)

Similar to the () ambiguity, with custom const generic types:

struct Foo(usize);
const Foo: Foo = Foo(5);

fn foo<A, B = (), const C: Foo, const D: Foo = Foo(7)>() {}

foo::<Foo, Foo, Foo>();

Is this calling foo::<Foo, (), { Foo(5) }, { Foo(5) }>() or foo::<Foo, Foo, { Foo(5) }, { Foo(7) }>()?

EDIT:

This doesn't help with making introducing new defaulted parameters backwards-compatible

fn foo<A, const C: Foo = Foo(7)>() {}
foo::<Foo, Foo>();

would be valid, introducing a new defaulted type parameter

fn foo<A, B = (), const C: Foo = Foo(7)>() {}
foo::<Foo, Foo>();

would now be ambiguous and require the {} to disambiguate that it's a const-generic-value.

2 Likes

The easiest example where this doesn't work is

struct Foo;

because Foo is now both a type and a value.

1 Like

For people who use const generics for the first time, it would be easier if the compiler didn't enforce a particular oder of types and consts. Then you don't have to remember which comes first.

I'm not sure why the fixed order would be more readable – consts are easily distinguishable from types by the const keyword. And when the generics get complex, bounds are usually moved to a where clause, which greatly helps readability.

In case it's not clear: we're proposing it should disambiguate as types, when it has to pick between a type and a const. So the first example would error because of too many type arguments, the second example would error because of too many type arguments, and the third example would compile.

This is not an issue in practice. The compiler can just tell you - as it already does. Rust has good tooling to tell you about and enforce standards. Personally we believe having a fixed order has lower cognitive load especially when reading stuff, and nobody has actually complained about it except for setting defaults. Which, there are other ways defaults could be handled that wouldn't involve changing the fixed order.

Further: If we ever get generic generics (and there are enough ppl who want generic generics - they're somewhat analogous to template templates in C++, see our previous post about them), well... they make things unreadable enough as it is, with a fixed order. Adding arbitrary order on top of them would make them completely unreadable. This is why this stuff about order and defaults is extremely important - whatever is decided now will get stuck with us "forever" (altho we can imagine ways to bodge one's way around it with edition changes - would probably be best to avoid going that way tho).

In particular, with generic generics, you'll be expecting to see nesting of <>. A fixed order should make it easier to work these out.

Surprisingly, this is actually how it behaves already with min const generics (playground)

fn foo<A, const C: usize>() {}
struct Foo {}
const Foo: usize = 5;
foo::<Foo, Foo>();
   Compiling playground v0.0.1 (/playground)
error[E0747]: type provided when a constant was expected
 --> src/main.rs:6:12
  |
6 | foo::<Foo, Foo>();
  |            ^^^
  |
help: if this generic argument was intended as a const parameter, surround it with braces
  |
6 | foo::<Foo, { Foo }>();

I really did not expect that :thinking:

I'm sure this has been discussed: if the ordering is fixed, then const parameters could be separated with a semicolon ; to disambiguate. Although it would require an ugly leading semicolon in a few cases:

const Foo = 17u32;
fn foo<A = (), const C: u32>() {}
foo::<; Foo>();

An option I do not recall being discussed would be to require const in the call site as well. It'd be wordy, but perhaps not too bad, as the majority of parameters would stil be types, and remove the braces requirement.

let a: Array<u8, const HEADER_SIZE + PAYLOAD_SIZE>;
let m: Matrix2D<MyStruct, const 32, const 16>;

// or for the aforementioned ambiguous `Foo` cases:
foo::<Foo, const Foo>();

It would also work nicely with associated consts: Array<const LEN = 5>.

3 Likes

You can just use {} like you can today.

I don't understand the benefit of separating types from consts. It would be like requiring all function declarations to sort their parameters by type. How does that benefit anyone?

Surely this:

struct Container<Foo, const FOO_COUNT: usize,
                 Bar, const BAR_COUNT: usize,
                 Baz>

is clearer than:

struct Container<Foo, Bar, Baz,
				 const FOO_COUNT: usize,
                 const BAR_COUNT: usize>

Indeed, I don't understand why we require separating types from lifetimes, either. By the same token as above, I'd prefer this:

struct Something<'foo, Foo: SomeTrait + 'foo,
			     'bar, Bar: SomeTrait + 'bar>

over the currently required:

struct Something<'foo, 'bar,
				 Foo: SomeTrait + 'foo,
			     Bar: SomeTrait + 'bar>

I suppose that putting lifetimes first makes it easy to tell whether a declaration has any lifetimes or not, which can then… be an incomplete hint as to whether it's 'static or not? Yeah, I don't get it.

5 Likes

Try something with generic generics:

struct Something<'foo, 'bar,
                 Foo: SomeTrait + 'foo,
                 Bar: SomeTrait + 'bar,
                 const FOO_COUNT: usize,
                 const BAR_COUNT: usize,
                 Baz<'a, T, const N: usize>,
                 Qux<'a, T, const N: usize>>

vs any other order which would make this a pain/too much cognitive load to read.

struct Something<'foo,
                 const FOO_COUNT: usize,
                 'bar,
                 Foo: SomeTrait + 'foo,
                 Baz<const N: usize, 'a, T>,
                 Bar: SomeTrait + 'bar,
                 const BAR_COUNT: usize,
                 Qux<T, 'a, const N: usize>>

(without even getting into where and where for...)

That's not allowed today, Rust doesn't have generic generics.

But the way I'd like to write it, if the order of generic parameters had no limitations, is this:

struct Something<
    'foo, Foo, const FOO_COUNT: usize,
    'bar, Bar, const BAR_COUNT: usize,
    Baz<'a, T, const N: usize>,
    Qux<'a, T, const N: usize>,
>()
where
    Foo: SomeTrait + 'foo,
    Bar: SomeTrait + 'bar;

which I think is pretty readable.

But even if the order of generics isn't enforced, nothing prevents you from sorting them anyway.

See, but you are following an order there.

But there's no way for the compiler, or clippy, or whatever, to enforce the order like that. So it's better to pick something it can enforce, and fixed order is something it can enforce. While your example is readable, the original example with fixed order is just as readable - but it can be easily enforced.

So, the two orders are equally readable, and neither one causes any problems - but the compiler should make one of them a hard error?

2 Likes

Nothing is stopping Clippy from adding a lint for const generics parameters that aren't written in a certain order. Whether or not such a lint is a good idea is another question entirely.

I don't think any order needs to be enforced. Sure, order is more readable than chaos, but Rustc usually doesn't concern itself with readability. Rust doesn't give warnings or errors when the code isn't properly indented, or when a function has 8 arguments (or 8 generic parameters), or when all your variable names are single letters. That's what clippy and rustfmt are for. Most people really care about readability. That's why they use rustfmt/clippy even though nobody forces them to. So the argument that we have to enforce a specific ordering for readability sounds unreasonable to me.

3 Likes