This is a rough draft to get the discussion started.
(TODO: this needs to be more precise with respect to the terminology, in particular the technical terms const variables
, const parameters
, const expression
, const projection
, etc. should all be used correctly here)
Summary
This RFC allows const
to be used in function argument position. This RFC does not propose an alternative syntax to the RFC2000: const
generics, but rather builds upon it to improve ergonomics in some situations. RFC2000: const
generics is still required for any of this to work.
Motivation
(I am biased towards stdsimd
, so that’s what I can write about here, but maybe you’ll have some ideas about how to improve this section with other examples?)
Many vendor intrinsics are specified to take function arguments that must be compile-time constants:
// note: this is not C, `const` here means compile-time constant
__m128 _mm_blend_ps (__m128 a, __m128 b, const int imm8);
Users building abstractions around these intrinsics (stdsimd
, faster
) would benefit from being able to propagate this invariant. [RFC 2000: const
generics] can be used to propagate these invariant:
fn _mm_blend_ps<const imm8: i32>(a: __m128, b: __m128) -> __m128 { ... }
However, these vendors intrinsics in C and C++ are written using a bit of compiler magic and can be used like this:
constexpr auto c = ...;
auto r = _mm_blend_ps(a, b, c);
in a form that directly maps to the vendor spec. However in Rust, the function calls look like this:
const C: i32 = ...;
let r = _mm_blend_ps::<C>(a, b);
// and like this:
let r = _mm_blend_ps::<{2 * C}>(a, b);
I think that ergonomics do matter, and Rust should be able to call this like C as well:
const C: i32 = ...;
let r = _mm_blend_ps(a, b, C);
let r = _mm_blend_ps(a, b, 2 * C);
A consequence of this is that const
generics become a bit easier to teach, at least for beginners. For example, after teaching a beginner the difference between:
const X: i32 = 3;
// vs
let x: i32 = 3;
One can mention that functions can take const
arguments like this:
fn foo(const X: usize);
// and call them like this:
foo(42);
Without this feature, writing
fn foo<const X: usize>();
foo::<42>();
One would need to explain “angle brackets”, maybe a bit about generics, turbo-fish, why do we need {2 * N}
when using const generics, etc.
Guide-level explanation / Reference
The motivation was a bit ad-hoc, but the feature itself is not. This RFC doesn’t let you write in Rust any kind of programs that you couldn’t write before using const
-generics, it just adds a different syntax for them. This is similar, but not the same, as adding impl Trait
as an alternative syntax for generics in function arguments.
This RFC allows const
to be used in function argument position:
fn foo(const X: u8) { ... }
The function foo
above is just different syntax for:
fn foo<const X: u8>() { ... }
The exact same semantics of [RFC 2000: const
generics] apply here:
- the value of the constant
X
is part of the function’s type - the constant could be used in where clauses (when
const
generics supports that) - the constant can be used as the length of an array
[T; X]
(whenconst
-generics supports that) - users can’t take pointers to functions with
const
-function arguments because they are generic functions - same object-safety rules as for
const
-generics apply - structural equality: only types which have the “structural match” property can be used as const parameters (that is,
floats
are not allowed). - …
From the [RFC 2000: const
generics],
they can be used:
// 1. As an applied const to any type which forms a part
// of the signature of the item in question
fn foo(a: [i32; N], const N: usize); // OK
// (2: does not apply)
// 3. As a value in any runtime expression in the body
of any functions in the item.
fn bar(const N: usize) -> usize { N }
// 4. As a parameter to any type used in the body of any
functions in the item:
fn baz(const N: usize) {
let x: [i32; N];
<[i32; N] as Foo>::bar();
}
// (5: does not apply)
They cannot be used (just like generic consts) in the construction of consts, statics, functions, or types inside a function body. That is, these are invalid:
fn foo(const X: usize) {
const Y: usize = X * 2;
static Z: (usize, usize)= (X, X);
struct Foo([i32; X]);
}
Adapting another example from the [RFC 2000: const
generics], the return value of const
expressions is treated as projections. That is,
this does not typecheck, because N + 1
appears in two different types:
fn foo(const N: usize) -> [i32; N + 1] {
let x: [i32; N + 1] = [0; N + 1];
x
}
but this does, because it appears only once:
type Foo<const N: usize> = [i32; N + 1];
fn foo(const N: usize) -> Foo<N> {
let x: Foo<N> = Default::default();
x
}
Note that for this second example to work we need [RFC 2000: const
generics] to implement the type alias. That is, this RFC builds on [RFC 2000: const
generics] and it is not proposing replacing its syntax.
There are two syntactic differences with respect to [RFC 2000: const
generics] :
-
One cannot pass
const
-function arguments via turbofish::<>
when calling functions:fn foo(const X: u8) { ... } foo(0); // OK foo::<0>(); // ~ERROR: function does not accept const-generic arguments
See alternatives for a way to allow this.
-
One does not need the braces (
{2 * N}
) to disambiguate:const N: usize = 3; fn foo<const I: usize>(); foo::<{N * N}>(); fn bar(const I: usize); bar(N * N);
Reference-level explanation
This is just syntax sugar for const
-generics. Does this introduce any problems? I don’t know. I trust that you guys won’t let me be wrong on the internet.
Drawbacks
Like every feature, one drawback is increased implementation complexity.
Another drawback is that the syntax might throw away programmers coming from languages where const
does not have the same meaning as in Rust (C, C++, …). That is, they might think that here:
fn foo(const X: usize) { ... }
X
might just be “immutable” rather than a “compile-time constant”. AFAICT this problem already exists:
const X: i32 = 3;
// vs
let x: i32 = 3;
Programmers coming to Rust from these languages are going to need to learn the true meaning of const
in Rust at one point or another. Otherwise the compiler will teach them the true meaning of const
via a beautiful error message when they try to do something that’s not allowed.
There is also concern that this feature will set a precedent for allowing passing other kinds in function argument position. This RFC does neither prevent those feature from happening nor proposes them. Some languages like Swift allow this as:
doSomething(myType: T.self) // call site
This looks nicer in Swift than what it would look like in Rust right now because Swift has named parameters.
Rationale and alternatives
If we could “do nothing” we would not be participating in the internals forum. Jokes aside, we could map const
in function arguments to something different than const
generics, but I don’t know what that would be.
The Guide-level explanation / Reference
section states that “one cannot pass const
-function arguments via turbofish ::<>
when calling functions”. We could allow this by declaring that const
function arguments are located at the end of the type-argument list in the order in which they are declared but then they must be omitted from the function arguments when calling the function:
fn foo<'a, 'b, T, U, const X: usize>(
a: &'a T, const Y: usize, b: &'U, const Z: usize
) { ... }
foo::<i32, i32, /* X: */ 0, /* Y: */ 1, /* Z: */ 2>(&a, &b); // OK
This would only work as long as we never plan to add any new kind of arguments inside <>
that need to come after type-parameters and const-generics. If we ever do that, this ad-hoc approach would break, because it is not very future proof.
Unresolved questions
Probably many.
-
Are there any arguments arguments for allowing
const
in function arguments for “consistency” reasons? One cannot uselet
in function arguments, e.g.,foo(let x: i32)
, so being able to usefoo(const X: i32)
might be inconsistent? -
We might want to allow
const
in patterns someday. I don’t see anything in this RFC that would prevent this, but if we ever had something like:struct A { x: i32, const y: i32, // const in struct fields } // const here means two very different things: fn foo(const X: usize, A { x, const y }: A) { ... }
Having said this,
const
's do not occupy memory, so it will probably make more sense to allowstructs
to haveassociated const
than to haveconst struct fields
.