Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Polymorphic Numeric Constants #1945

Closed
wants to merge 4 commits into from

Conversation

retep998
Copy link
Member

@retep998 retep998 commented Mar 6, 2017

rendered

Note that I don't particularly mind what the syntax to define a polymorphic constant is, as long as the user can still use the constant the same as any other constant.

# Alternatives
[alternatives]: #alternatives

* One alternative is to use macro constants, which involves defining a macro for each constant, and invoking the constant via `FOO!()` instead of `FOO`. It is verbose and ugly, clutters the global macro namespace, and will probably uncover some performance regression in rustc.
Copy link
Member

@est31 est31 Mar 6, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cuviper has suggested another alternative over at irlo: making it possible to drop the () at the macro invocation, requiring you only to type FOO!. Ofc still one character lost, and until macros 2.0 the global namespace problem still persists.

@eddyb
Copy link
Member

eddyb commented Mar 6, 2017

So there are two problems here:

  1. We may want to infer const / static types from straight-forward initializers (type inference for consts/statics #1349)
  2. It's not super clear here that that constant is polymorphic, i.e.
const INTEGER = 273;
// is actually, under the hood:
const INTEGER<T: FromIntLiteral>: T = 273;

Now whether we want that syntax is another discussion. It's also plausible to treat uninferred literals as either an error or leave them polymorphic, instead of defaulting to i32, for compatibility with 1.

IMO we should have something like #143, if not for custom types, at least for builtin ones.

@crumblingstatue
Copy link

crumblingstatue commented Mar 6, 2017

Maybe a contextual keyword like poly const FOO = ...; could be used to avoid conflicts with inference and improve clarity.

I definitely want to support const expressions for their initializers. const BAZ = FOO + BAR is not uncommon. Not allowing these would severely limit the usefulness for polymorphic consts.

@nrc nrc added the T-lang Relevant to the language team, which will review and decide on the RFC. label Mar 6, 2017
@eddyb
Copy link
Member

eddyb commented Mar 6, 2017

Oh and I vehemently disagree with the idea of calling these "untyped". Rust doesn't have such a concept and if it we try to squeeze it in, it might even be unsound, you need to represent this as polymorphism.

@retep998 retep998 changed the title Untyped Numeric Constants Polymorphic Numeric Constants Mar 6, 2017
@eddyb
Copy link
Member

eddyb commented Mar 6, 2017

FWIW, C++'s equivalent-in-power feature is template'd constexpr globals.

@TimNN
Copy link
Contributor

TimNN commented Mar 6, 2017

@retep998: The rendered link in the top post needs updating ;)

@retep998
Copy link
Member Author

retep998 commented Mar 6, 2017

A simple rg "#define\s+\w+\s+[\s(0xX-]*[0-9a-fA-F]+(L|l|U|u|ll|LL)?[ \s)]*(//.*|/\*.*)?$" of the Windows SDK headers indicates a minimum of 93,694 integer constants that do not have any type specified and are plain literals without arithmetic or other functions/macros involved.

@TimNN Fixed

@crumblingstatue
Copy link

bindgen could also benefit from this.

It translates

typedef int sfBool;
#define sfFalse 0
#define sfTrue  1

to

pub const sfFalse: ::std::os::raw::c_uint = 0;
pub const sfTrue: ::std::os::raw::c_uint = 1;
pub type sfBool = ::std::os::raw::c_int;

causing type mismatches between sfBool and its constants.

@kornelski
Copy link
Contributor

Rust already has this feature for literals:

fn main() {
    let x:&str = 1;
}

= note: expected type &str
= note: found type {integer}

So if the magic {integer} type could be inferred for constants, that'd be great!

@eddyb
Copy link
Member

eddyb commented Mar 7, 2017

@pornel That's an inference variable which is local to one constant initializer or function body, it's useless across them, and has to be fully resolved locally just like the more general _ inference variables.

@@ -0,0 +1,80 @@
- Feature Name: polymorphic_numeric_constant
- Start Date: 2017-04-06
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's hope we have a time machine built in Rust :-)

@gnzlbg
Copy link
Contributor

gnzlbg commented Mar 7, 2017

@eddyb

FWIW, C++'s equivalent-in-power feature is template'd constexpr globals.

It's a bit more complicated than that. You have two features: variable templates (N3651) and inline variables (P0386). You really need both of them if you want to be able to use generic globals without ODR/linkage issues.

I also like the syntax:

const INTEGER<T: FromIntLiteral>: T = 273;

But I have two questions that do not seem to be addressed by the RFC:

    1. How do we construct INTEGER from the value 273? It should work for user defined types as well, or at least, there should be a path forward to such an extension without major issues.
    1. How do we specialize these constants, or give them values for different types, or different groups of types? How does this interact with specialization?
    • Motivation: Say I want a generic constant const HIGHEST<T: FromIntLiteral>: T= /* largest repr value of T */;. How do I give that constant different values for different Ts ?

Given both these questions, I think we could really just phrase this as "syntax sugar" for associated constants. That is:

const INTEGER<T: FromIntLiteral>: T = 273;

would just desugar to:

pub trait INTEGER<T: FromIntLiteral> {
  const VALUE: T = 273;  // default constant value
}

where 273 is a default value for the constant. Using INTEGER::<T> would just then desugar to INTEGER::<u32>::VALUE.

This way, we could override the default value by implementing the Integer INTEGER trait for different types, and future features like specialization would "just work" once they get stabilized.

Or, in other words, the following two features:

    1. Creating a generic constant just desugars into a generic trait with its same name, and same generic parameters, and a constant field named VALUE with the same parameter bounds and defaulted to the value assigned to the generic constant on declaration, if any (this allows const INTEGER<T: FromIntLiteral>: T; without a default value).
    1. Accessing a generic trait specified via a generic constants as trait::<params> desugars to trait::<params>::VALUE.

I am worried about 2., because I would really like that if we add this kind of desugaring, it should work for all traits with a VALUE field that's an associated constant, not only the ones specified via generic constants, but trait::<params> creates a trait object, so I guess this syntax would be ambiguous, in particular since we might end up using it at the type-level a lot once we get type-level integers (in C++ I use variable templates in the type-level a lot).

@eddyb
Copy link
Member

eddyb commented Mar 7, 2017

@gnzlbg Why do you think you need a trait? We can add generic parameters to regular const items as well.

@gnzlbg
Copy link
Contributor

gnzlbg commented Mar 7, 2017

@eddyb Mainly because it opens the door of using specialization. A const fn with generic parameters wouldn't be specializable, and a trait method cannot be const fn, so... is there an alternative to a trait + associated const if we want to be able to use specialization?

@eddyb
Copy link
Member

eddyb commented Mar 7, 2017

This way, we could override the default value by implementing the Integer trait for different types,

Oh, did you mean INTEGER? In any case, this would be valid with polymorphic constants:

const FOO<T: Foo>: T = T::VALUE;

@gnzlbg
Copy link
Contributor

gnzlbg commented Mar 7, 2017

Oh, did you mean INTEGER?

Yes, sorry about that, I just fixed it :)

@gnzlbg
Copy link
Contributor

gnzlbg commented Mar 7, 2017

So I guess the TL;DR: of my post is that traits + associated const already allow us to do this in a more powerful way, but wanting to define a parametrized const is common enough, that we should have sugar for it. If the user realizes afterwards that e.g. it needs to specialize the value of a parametrized const for some type, it should still be possible to do so with minor or none API breakage. Ideally it would just need to rewrite the const as the trait it desugars to and everything else would still work.

@joshtriplett
Copy link
Member

I like the idea of using traits and generics for this. At a minimum, that seems worth mentioning as an alternative, but it seems to me like the preferable approach.

We'd need syntax for generics on consts, but that seems like a reasonable thing to have. That way, you could declare a const that has the same property as just writing a numeric literal: it'll work as many different possible types.

# Alternatives
[alternatives]: #alternatives

* One alternative is to use macro constants, which involves defining a macro for each constant, and invoking the constant via `FOO!()` instead of `FOO`. It is verbose and ugly, clutters the global macro namespace, and will probably uncover some performance regression in rustc. Macros 2.0 may get rid of the global namespace pollution, and an RFC could make calling the macro as simple as `FOO!` but it is still far from ideal.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Macros would be my favoured solution since it doesn't require extending the type system in any way. We would make these much less verbose to declare and scopable as part of macros 2.0. We could try and handle not requiring the (). It doesn't seem far from ideal at that point.

@joshtriplett
Copy link
Member

@nrc Why do you prefer macros instead of traits/generics?

@nrc
Copy link
Member

nrc commented Mar 9, 2017

@joshtriplett macros already exist and we plan to make the changes anyway. Generics require extending the type system.

@joshtriplett
Copy link
Member

@nrc We wouldn't need to extend the type system to allow const generics; it's the same kind of inference we already use in other places, like functions returning a generic type.

I wonder if we could split this into two separate issues: 1) const generics, and 2) some kind of trait for numeric literals. The former seems really straightforward; the latter seems like the complicated bit here.

Does that sound plausible?

@nrc
Copy link
Member

nrc commented Mar 9, 2017

Potential macro syntax:

// decl
macro FOO { 42 }

// use
let x = FOO!;

and if we can't make the lack of ():

// decl
macro FOO() { 42 }

// use
let x = FOO!();

@nrc
Copy link
Member

nrc commented Mar 9, 2017

@joshtriplett part 1 is an extension, because it doesn't work today. It is however a small extension (probably, there is some risk still). part 2 is more worrying - it is a tricky part of type inference already and adding a trait to the mix seems non-trivial.

I'm not sure either is worth doing, unless there is motivation beyond that given here (because this use case can be addressed with macros).

@Ericson2314
Copy link
Contributor

Ericson2314 commented Mar 9, 2017

@nrc I don't the inference aspects of 2 are too hard. To my knowledge, we don't have any sort of let-polymorphism in rust ATM, and this would not change that (and thus our i32 default won't be fighting a new true "most general type"). Integer literals are already polymorphic with a constraint ("integerness") that simply would be reified as an actual trait.

The hard part would be generalizing to allow user-definable implementations supporting arbitrary precision. We'd need const fn methods, or some crazy (but awesome!) trait + proc macro mashup.

@nikomatsakis
Copy link
Contributor

@rfcbot fcp postpone

We discussed this RFC in the @rust-lang/lang meeting yesterday and decided to move to postpone the RFC. This is with some regret, since I think that we all felt the motivation was fairly strong. In particular, it'd be nice to make numeric constants ergonomic, and it's true that FFI (and Windows integration!) is an important application. I'm particularly keen on finding ways to improve FFI ergonomics and approachability.

However, ultimately we felt like the approach outlined in this RFC probably wasn't the approach we would want, and the proposal presents a number of complications. The first is that the syntax it proposes, const Foo = 22, strongly suggests that this is an ordinary constant with an inferred type, when in fact it would be something rather different.

If we wanted to put this on some kind of solid foundation, there would be perhaps three ways to do it, both of which expose non-trivial complications. The first is, as @eddyb suggested, to use generics, so we could understand const Foo = 22 as some kind of shorthand for a generic constant which infers its type from the use-site, rather like Default::default():

const Foo<T: Integer>: T = 22

To do this would also require some sort of special trait (here I called it Integer) that represents "a built-in integer type". This trait would be fairly special, since for example we'd be able to assign the integer literal 22 the type T because T: Integer (in other words, we have a kind of integer literal overloading going on here). Generic constants seem like a good idea, and literal overloading is also potentially a good idea, but neither is a particuarly trivial idea. If we want to have them, it'll take quite a bit of design work and iteration.

Another complication is that the handling of integer literals in rustc today is kind of tricky in some ways. For example, we currently use a distinct kind of type variable to represent integer types. Unfortunately, the type-checker takes advantage of the fact that it knows that 22 has a type that is known to be "some built-in integer type" during unification, so e.g. it will fail to unify such a variable with a struct type. It'd be nice to try and stop doing that, and instead using regular type variables with a special trait (rather like what was suggesed in the last paragraph), but this may not be backwards compatible since it will interact with inference (we'd have to experiment). This fact may suggest that we wind up formalizing the concept of an "integral type" as a distinct "subkind" of normal types, but I wouldn't particularly want to do that -- it would potentially help with this RFC in some ways.

Finally there are the complications that others mentioned around precision and so forth.

None of these points are blockers, but they are all substantial enough to suggest that the time is not ripe for this RFC, and that if we were to pursue it as a kind of "on the side" endeavor, it has a high probability of requiring a lot of time and effort which would distract from other roadmap goals. Hence the decision to move to postpone.

@rfcbot
Copy link
Collaborator

rfcbot commented Mar 10, 2017

Team member @nikomatsakis has proposed to postpone this. The next step is review by the rest of the tagged teams:

No concerns currently listed.

Once these reviewers reach consensus, this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!

See this document for info about what commands tagged team members can give me.

@Ericson2314
Copy link
Contributor

Ericson2314 commented Mar 11, 2017

Maybe in the meantime we can just get plain old polymorphic constants?

Generic constants seem like a good idea, and literal overloading is also potentially a good idea, but neither is a particuarly trivial idea.

The first part ought to be trivial. e.g. given that the first works, what's so hard about the second?

const fn asdf<T>() -> [T; 0] { [] } 
const asdf<T>: [T; 0] = [];

This fact may suggest that we wind up formalizing the concept of an "integral type" as a distinct "subkind" of normal types, but I wouldn't particularly want to do that

Perhaps there should be an issue for replacing the special inference variables with an (unnameable for now) trait internally? i.e. it would be a shame for backwards-incompatability to bite us here and perhaps somebody (not on the core time, not bound to the roadmap) could investigate?

Also, mostly off-topic, I've been musing being able to pun between bounds and subkinds. Seems useful, and related to #1733.

@rfcbot
Copy link
Collaborator

rfcbot commented Mar 21, 2017

🔔 This is now entering its final comment period, as per the review above. 🔔

@rfcbot rfcbot added the final-comment-period Will be merged/postponed/closed in ~10 calendar days unless new substational objections are raised. label Mar 21, 2017
@rfcbot
Copy link
Collaborator

rfcbot commented Mar 31, 2017

The final comment period is now complete.

@aturon
Copy link
Member

aturon commented Apr 10, 2017

Closing as postponed, as per the summary comment.

@aturon aturon closed this Apr 10, 2017
@strega-nil
Copy link

@gnzlbg (random) I'd just like to point out that templated things are automatically inline, and therefore you don't need to worry about ODR-usage. That's why people wanted to add actual inline variables (or at least it was an argument for them); the compilers already had to have the structures in place to allow inline variables.

@burdges
Copy link

burdges commented Apr 10, 2017

Just for whenever this idea returns : If the syntax should be roughly const FOO<T: Integer>: T = 25, due to whatever issues with const FOO = 25, then perhaps a shorthand like const FOO = 25i along with 25u and 3.0f might work, as opposed to say = impl 3.0. You could use 25i wherever some future magic numerical trait bound made sense.

@strega-nil
Copy link

I would rather just have a FromIntegerLiteral trait that any type can implement (and FromFloatingLiteral as well).

@petrochenkov
Copy link
Contributor

const FOO<T: Integer>: T = 25 is verbose

Seems like the shortcut const FOO: impl Integer = 25 from #1951 is applicable here as well, but it have to mean any Integer which is a bit strange if you compare it with equivalent const fn FOO() -> impl Integer { 25 } where it means the opposite thing.

@burdges
Copy link

burdges commented Aug 24, 2017

I noticed that polymorphic constants, even without polymorphic numeric literals, would make returning from fn(..) -> Result<(),Error> easier, via say

pub const OK<E>: Result<(),E> = Ok(());

@retep998
Copy link
Member Author

Even if we went with const FOO<T: FromIntegerLiteral>: T = 273; I still wouldn't mind that verbosity because I'd likely have it wrapped in a macro such as define!{FOO 273}.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
final-comment-period Will be merged/postponed/closed in ~10 calendar days unless new substational objections are raised. T-lang Relevant to the language team, which will review and decide on the RFC.
Projects
None yet
Development

Successfully merging this pull request may close these issues.