pre-RFC `const` function arguments

I expect the attribute can be extended to forward “constness”, allowing us to buy a lot of time.

#[inline(always)] // <- needed to forward constness
#[rustc_arg_required_const(0)]
fn foo(a: u32) {
    bar(a);
}
#[rustc_arg_required_const(0)]
fn bar(a: u32) {
}

(Sorry I didn’t check the stdsimd source code for the actual implementation, I just assumed the situation is similar to the atomic types)

Yes @kennytm: once the attribute can do that we will be able to remove a lot of cruft inside stdsimd and implement some one missing intrinsics. However, we can't use #[inline(always)] in stdsimd because LLVM then inlines when it shouldn't and that breaks things (it inlines across incompatible target features, and this breaks code that uses run-time feature detection, among other things).

In any case, we might be able to over-come these problems with the attribute, and doing so shouldn't be that hard once the RFC2000: const-generics is implemented.

Unless we decide to stabilize that attribute, users still won't be able to use it on stable.

This RFC does not proposing adding const as part of patterns. AFAIK const as part of patterns doesn't work yet (what would this even mean?), e.g.

struct A { a: i32, b: i32 }
let A{ a, b }: A = A{ a: 2, b: 3 }; // OK
const A{ a, b }: A = A { a: 2, b: 3 }; // ~ERROR

I can imagine that we might want to make that work someday though, so this RFC should not prevent that from happening.

From a syntactic point-of-view, if we ever get const in patterns, we might be able to allow this in const-function arguments without breaking backwards compatibility like this:

fn foo(const A{ a, b }: A) {}

The question is whether const generics would play along. I have no idea.

Yep :slight_smile:

Overall, I'm still very hesitant about the feature, though... things like this might offset the teachability benefits:

I guess what makes this a bit tricky is that it would seem weird that you can't use certain types as const arguments (e.g. floating point numbers strike me as the most likely first source of confusion)... beginners who don't understand const-generics won't understand structural equality :confused:

Could you please remind me what's bad in those useful lines of code?

Good point, this is a problem that const variables do not have. This reinforces the idea that const variables are not equivalent to const-function arguments (which are equivalent to const parameters). Having said this, the error messages for this do not need to mention structural equality or any other technical terms. const-generics is going to have these problems and if we can have good error messages there we can have them here as well. This is important because RFC2000 makes it very clear floating point values will never be usable in const generics because they are not reflexive, so I would guess that floating point types in particular will probably get their own custom error message because this is a common error to make.


@leonardo From the RFC2000:

This restriction can be analogized to the restriction on using type variables in types constructed in the body of functions - all of these declarations, though private to this item, must be independent of it, and do not have any of its parameters in scope.

D language has templates more like C++, and it allows all that code:

void foo(size_t x)() {
    enum size_t y = x * 2;
    static size_t[2] z = [x, x];

    struct Foo {
        int[x] arg;
    }
}
void main() {
    foo!5();
}

The RFC2000 discusses this in detail: it mentions allowing that as an extension, and it discusses why it doesn’t do so initially. I’d recommend reading the RFC2000 and its PR for context.

Adding that feature in the future would mean that it becomes usable with const function arguments as well, but I would prefer if this thread does not become the place to discuss doing that. This RFC focuses on a different issue.

Yes, sorry for overriding your thread. I remember Walter Bright trying to implement your idea in the D language (enum function arguments) and failing.

That's interesting, do you have links or context that explains why, or do you happen to know what the issues are? D is a completely different language, so maybe what applies to D does not apply to Rust, but learning what failed there might help.

D isn’t a completely different language, despite D and Rust generics have a different design (you can add constraints to the type arguments of D generics, but the compiler doesn’t enforce they are sufficient for their successful compilation, and the exact template code is verified only at instantiation time). But I agree that the D compiler is quite different from the Rust compiler so probably there’s no insight that Walter failure for Rustc.

I have failed finding the D thread, it’s an old one and the forum have moved several times, lot of stuff could harder to find.

I vaguely remember Walter failed because there was too much mixing between run-time and compile-time stuff. I suspect the problem was similar of having the ctfe boolean variable (that’s true in a function if that function is run at run time) as enum instead as a run-time variable.

Walter tried to implement this idea for ergonomic reasons similar to yours, that is to make D templates “more human”.

Elsewhere I have suggested to introduce in Rust (and D) a feature similar to Ada 2012 Static_Predicate: http://www.ada-auth.org/standards/12rat/html/Rat12-2-5.html

Perhaps your idea could be used/adapted to implement that feature…

1 Like

As an interesting data point:

Swift doesn’t support const generics, but even for type parameters, it has a mechanism specifically to avoid the need for explicit function specialization (i.e. turbofish), which it doesn’t support at all.

For example, to do the equivalent of transmute, you can write:

let transmuted = unsafeBitCast(foo, to: Int32.self)

The unsafeBitCast function is declared as

public func unsafeBitCast<T, U>(_ x: T, to type: U.Type) -> U

This works because Int32.Type is a singleton type with Int32.self as the sole instance (and that works for any type). However, it requires some special-casing, because T.Type looks like an associated type, but normally you can’t do type inference backwards from an associated type to the base type. (Associated types in Swift have similar semantics as in Rust.) And for that matter, I find it a bit strange: why can’t you just write to: Int32? In Rust, the reason you can’t just write a type in expression position is to avoid parser ambiguities with angle brackets, but adding .self at the end doesn’t help with that (and Swift has a different solution to the angle bracket issue).

But anyway, you can see how it’s nice to put generic parameters into the regular flow of function arguments, allowing for a more natural ordering. Swift probably benefits more from this than Rust would, due to its pervasive use of argument labels - i.e. cast(value, to: Type) clearly flows better than cast<Type>(value), but you couldn’t say the same for cast(value, Type). Still - getting back on topic - when it comes to value parameters (const generics), I think there are more cases where it would be useful.

I think I’d like to see const generics implemented before we talk about sugaring them. :slight_smile:

14 Likes

we can make the point that const in Rust is equivalent to constexpr in C++ (mostly) - i.e., what const should have meant :wink:. It’s also not on the type, it’s on the binding, which should make it more obvious.

1 Like

That’s fair. I submitted this pre-RFC for two reasons:

  • const generics aren’t implemented yet, and if this is something that we’ll eventually want to pursue then maybe those implementing const generics should be aware of it and raise issues about it while implementing const-generics (instead of x time later when nobody remembers the gory implementation details).

  • there is a stopgap solution (the PR @kennytm mentioned) being merged into master as we speak, and that is part of a feature (SIMD) that should be part of the Rust 2018 epoch (that is, it will become stable some time earlier this year). When adding stopgap solutions to language limitations that will surface in the API of std library functionality that will become stabilized “soon”, I think that it makes sense to at least explore in a pre-RFC form what that stopgap solution would look like if it were turned into a “real” language feature. Mainly to be sure that we don’t stabilize a stopgap that can’t either be replaced by something better in the future, that is completely broken by design, or that prevents us from doing the right thing in the future.

Does this make sense?

1 Like

To me, const generics are about lifting values (const values in this case) to the type level (i.e: dependent types). If const generics are permitted within (args) of function application fun(args), then so should other type level arguments. So for the function definition fn identity<T>(x: T) { x } the call identity(u8, 1u8) would be permitted as the equivalent of identity::<u8>(1u8). Anything else would be inconsistent to me. So I’m currently against this proposal.

5 Likes

For lifetime parameters one does not need to use < >:

fn foo<'a>(x: &'a i32) {}
// can be written as just:
fn foo(x: &i32) {}

For type parameters RFC1951: universal impl Trait was merged. This works today:

#![feature(universal_impl_trait)]
use std::fmt::Display;
// This is just another way of writing
// fn foo<T: Display>(x: T);
fn foo(x: impl Display) { println!("{}", x) }
fn main() { foo(0); }

That is, the language can already do this in "some form" for both lifetimes and types. Obviously, types are not lifetimes, so the features for types and lifetimes are necessarily different. In both cases you can "pass" types and lifetimes via (args...) without turbofish by leveraging type deduction and lifetime inference. const are neither types nor lifetimes, so allowing something like this for consts would probably work differently as well.

This RFC proposes one possible way of doing this for consts, like we can already do for both types and lifetimes.

If const generics are permitted within (args) of function application fun(args), then so should other type level arguments. [...] Anything else would be inconsistent to me. So I’m currently against this proposal.

This proposal does not preclude adding those as a future extension in any way. If you feel strongly about those, write RFCs for them.

FWIW I think that an all-or-nothing mentality is not, in my opinion, very useful when discussing language improvements. Most language improvements that are actually merged are minimal incremental improvements over what's there. To me it is more useful to know whether this a direction in which we want Rust to go? Whether this is a step in that direction? Whether this is "the right step"? Whether this step prevents other steps that we might want to take? Etc.

2 Likes

AFAIK, not only are you not required to specify lifetimes in turbofish, you are not allowed to do so at all.

You also do not need to specify type arguments in turbofish in a majority of cases - but are permitted to do so, and I suspect this holds for const in a type level context in some cases as well (whether this is in the minority or majority it is too early to tell..).

I don't see how this argument about inference has any bearing on whether you should be able to move (some, specifically const) type level arguments from the type application site (turbofish site) to the value application site.

impl Trait in argument position provides for anonymous universal quantification, it does not permit you to specify the specific type of x in x: impl Display when calling foo, the type must (currently) be inferred.

To me, const in const generics are types (or they would at least be so in Haskell with DataKinds).

I should have clarified this part. My preference is that we not move in this general direction at all, so I won't write such an RFC. I think the separation between values and types is a good thing and I don't want to set precedent that changes this.

I agree, up to a point. I think we should think about what precedent certain changes set and what the "future work" might be. Having a coherent story is essential in language design.

1 Like

While replying to @centril I asked myself whether this can be considered some for of "const inference". It obviously is not: const-functions arguments are an explicit way of passing const parameters:

fn foo(x: [i32, N], const N: usize) { ... }
foo(x, 42);  // OK: equivalent to foo::<42>(x);
foo(x); // ~ERROR: missing const argument N
foo::<42>(x); 
// ~ERROR: missing const argument N
// ~ERROR: const function arguments cannot be passed via type paraments

With this restriction, const-generics are more powerful because if IIUC RFC2000 they allow “inferring” the value of the const:

fn foo<const N: usize>(x: [i32, N]) { ... }
let x: [i32, 42] = ... ;
foo::<42>(x); // OK
foo(x); // OK: N "inferred" to be 42
1 Like

IIUC RFC2000 const generics, which I am not sure I do (@withoutboats ?):

Today, types in Rust can be parameterized by two kinds: types and lifetimes. We will additionally allow types to be parameterized by values [...]

That explicitly states that const at the type level are not of kind type but of kind value. What RFC2000 does is introduce this new type of kind, and then it allows types to be parametrized by this new type of kind.

So while it might be useful to think of them as "types" they are not, they are values that can be part of a type.

Does this make sense? In any case this discussion is a bit tangential to this RFC I think.

I should have clarified this part. My preference is that we not move in this general direction at all, so I won’t write such an RFC. I think the separation between values and types is a good thing and I don’t want to set precedent that changes this.

I agree. Right now I don't think I want those other things in the language either. However, I obviously think that the part of that feature for consts stands on its own (or otherwise I wouldn't be writing this RFC).

I've extended the drawbacks section with your concern that this could lead to using types or lifetimes in function argument position just like Swift does. And clarify that this RFC neither prevents those features from happening nor proposes them, but that does not mean that they should be proposed.

EDIT: this is how Swift allows that btw: doSomething(myType: T.self). Named function arguments makes this a bit nicer than how doing something like that would look in Rust night now. But if Rust had named function arguments we might think about this differently.