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

type inference for consts/statics #1349

Open
nrc opened this issue Oct 31, 2015 · 38 comments
Open

type inference for consts/statics #1349

nrc opened this issue Oct 31, 2015 · 38 comments
Labels
T-lang Relevant to the language team, which will review and decide on the RFC.

Comments

@nrc
Copy link
Member

nrc commented Oct 31, 2015

We shouldn't require types for consts and statics unless necessary. const FOO = "foo"; or static bar = 42; should just work. I propose that we try to infer based only on the RHS, i.e., we do not look at uses of consts/statics. Type error if we can't infer based on that. Although this would break the rule that items must be fully annotated, it would make static/const more consistent with let.

@nrc nrc added the T-lang Relevant to the language team, which will review and decide on the RFC. label Oct 31, 2015
@nagisa
Copy link
Member

nagisa commented Oct 31, 2015

Ambivalent. I’m not using consts or statics compared to lets and I therefore don’t really find it to be a pain point to have specify a type of these.

I think type being specified might be better, since it is clear at the first sight what the type of const or static is. Similarly how’d you want to know what the type of function/method is. Having type specified is not as important for things that currently have an optional type (e.g. lets), because they can’t become a part of, or influence, public interface.

@petrochenkov
Copy link
Contributor

Dunno. You can do this in C++, but it's usually frowned upon (example)
Maybe type inference should be allowed, but only on private constants/statics.
(I had one weaker suggestion in the past - #406 (somewhat outdated with regards to arrays), it still leaves interfaces explicit.)

(On a related note it would be extra nice to have generic constants inferring their type on their use, like const A<T: __PrimitiveInt__> = 10;)

@sfackler
Copy link
Member

Dup of #296?

@withoutboats
Copy link
Contributor

I think at very least lifetime elisions of references in statics is a good idea. It feels very silly to write static FOOBAR: &'static str = "localhost:8080/foo" with the word static appearing twice.

@mbrubeck
Copy link
Contributor

mbrubeck commented Jun 6, 2016

I think at very least lifetime elisions of references in statics is a good idea.

#1623 has now been submitted proposing lifetime elision for statics.

@glaebhoerl
Copy link
Contributor

With respect to type inference of consts/statics (which is orthogonal and complementary to elision of the 'static lifetime), I had basically the same thought as @nrc in the OP: that it should be supported as long as the RHS consists exclusively of introduction forms (that is, literals), and doesn't reference any other const or static items. In this case the type of the RHS is "obvious" (true is always bool and "hello" is always &'static str and Foo { ... } is always Foo), and the explicit type annotation can be thought of as strictly redundant.

(There are a few wrinkles, like defaulting, notably for numeric literals, and polymorphic values like Option::None. These could just be conservatively still-not-inferred as well, at least at first.)

@llogiq
Copy link
Contributor

llogiq commented Jun 6, 2016

I think that doing type inference will risk hurting locality of the code. With elision, the effect is quite limited, whereas with full inference, you can have spooky action at a distance where the errors reported may be quite far away from the actual culprit.

@glaebhoerl
Copy link
Contributor

Not sure if you are agreeing or disagreeing or not even responding to me at all. :)

I agree that full inference would have those negative effects, which is why I think we should have the "literals only" restriction which, I believe, would avoid them.

@llogiq
Copy link
Contributor

llogiq commented Jun 6, 2016

@glaebhoerl so you'd want the full type for B in the following snippet?

static A = &[&["Hello", "Rust"], &["Foo", "Bar"]];
static B : ... = &[&A, &A]

@glaebhoerl
Copy link
Contributor

Right.

(Modulo elision of 'static, which again is orthogonal and complementary - as the example demonstrates.)

@durka
Copy link
Contributor

durka commented May 6, 2017

What if we inferred types for consts and statics, even based on use, but only within function bodies, leaving top-level type declarations alone?

@joshtriplett
Copy link
Member

I'd like to see this as well. I regularly find myself having to specify types in const declarations that the compiler should have no problem inferring.

@Ixrec
Copy link
Contributor

Ixrec commented Apr 20, 2018

I just noticed that #2010 is not linked to anywhere on this issue, which is pretty weird since that appears to be the last big discussion where new things were learned and problems raised that led to the current stalemate. For future readers, the biggest surprise was summarised by niko as follows:

based on the data that @schuster gathered, it looks like enabling i32 fallback would basically always pick the wrong type for simple things like const FOO = 22. This means you would still have to annotate cases of simple integer literals, and yet those appears to be the vast majority of constants!

@Boscop
Copy link

Boscop commented Apr 26, 2018

based on the data that @schuster gathered, it looks like enabling i32 fallback would basically always pick the wrong type for simple things like const FOO = 22. This means you would still have to annotate cases of simple integer literals, and yet those appears to be the vast majority of constants!

  1. Writing int types for const/static when they shouldn't be i32 is not a big deal, but there are many cases where the type CAN (and should) be inferred, often when arrays or structs are used, like this:
pub const COL_RED_1   : Col = COL_RED   .brightness(BRIGHTNESS_LOW); // const-fn
pub const COL_RED_2   : Col = COL_RED   .brightness(BRIGHTNESS_MID);
pub const COL_GREEN_1 : Col = COL_GREEN .brightness(BRIGHTNESS_LOW);
pub const COL_GREEN_2 : Col = COL_GREEN .brightness(BRIGHTNESS_MID);
pub const COL_BLUE_1  : Col = COL_BLUE  .brightness(BRIGHTNESS_LOW);
pub const COL_BLUE_2  : Col = COL_BLUE  .brightness(BRIGHTNESS_MID);
pub const COL_YELLOW_1: Col = COL_YELLOW.brightness(BRIGHTNESS_LOW);
pub const COL_YELLOW_2: Col = COL_YELLOW.brightness(BRIGHTNESS_MID);

The return type of the .brightness() const-fn is known to the compiler so none of these constants would have needed an explicit type. Keep in mind that const-fns will become much more widely used as they become more capable.

  1. What also occurs frequently are dependency chains of constants like this:
pub const PUSH_ENCODER_START: usize = 0;
pub const ROWS_START: usize = PUSH_ENCODER_START + 8;
pub const TOP_GROUP_0: usize = ROWS_START + 16;
pub const TOP_GROUP_1: usize = TOP_GROUP_0 + 1;
pub const TOP_GROUP_2: usize = TOP_GROUP_0 + 2;
pub const TOP_GROUP_3: usize = TOP_GROUP_0 + 3;
pub const STORE: usize = TOP_GROUP_0 + 4;
pub const LEARN: usize = STORE + 1;
pub const EDIT: usize = STORE + 2;
pub const EXIT: usize = STORE + 3;
pub const PRESET_PREV: usize = STORE + 4;
pub const PRESET_NEXT: usize = PRESET_PREV + 1;
pub const BOTTOM_GROUP_0: usize = PRESET_NEXT + 1;
pub const BOTTOM_GROUP_1: usize = BOTTOM_GROUP_0 + 1;
pub const BOTTOM_GROUP_2: usize = BOTTOM_GROUP_0 + 2;
pub const BOTTOM_GROUP_3: usize = BOTTOM_GROUP_0 + 3;
pub static FX_STATES: [usize; 8] = [STORE, LEARN, EDIT, EXIT, BOTTOM_GROUP_0, BOTTOM_GROUP_1, BOTTOM_GROUP_2, BOTTOM_GROUP_3];

With type inference it would be enough to annotate the first one as usize, and the types for all the others (incl. the array) could have been omitted! (Also it's annoying to have to count the size of static arrays before writing their type.)

  1. Due to lack of type inference for const/static I often find myself using a let binding in a function where (semantically) I should be using a function-local static/constant (when writing code in a hurry). Type inference would encourage using more function-local static/constants by reducing the inconvenience.

So I would really like to see type inference for const/static be added (not just function-local).

@joshtriplett
Copy link
Member

Another one I run into regularly is fixed-sized arrays, where I have to explicitly specify the type including the number of entries. I'd like to omit that number.

@Centril
Copy link
Contributor

Centril commented Apr 26, 2018

@Ixrec It seems to me that for const items, the type could be held abstract / polymorphic in those cases as {integer} (or the most general type) and not pick a specific concrete type.

@Boscop
Copy link

Boscop commented Apr 26, 2018

@Centril There is also the case where the crate that defines a constant doesn't use it in its own code, but exports it, so it can't be inferred (so it would be a compile-time error "type needs to be specified").

@Centril
Copy link
Contributor

Centril commented Apr 26, 2018

@Boscop That's fine; You can do it for non-pub items then. However, you could potentially hold the type abstract in the crate and so it could work again.

@durka
Copy link
Contributor

durka commented Apr 26, 2018

@joshtriplett There's a macro for that. It shouldn't be necessary though, I agree.

@Boscop
Copy link

Boscop commented Apr 27, 2018

The macro is clever but I wouldn't use it because it makes the code harder to read (also for others). I'd prefer to have this built-in..

@Centril It should still infer the type of pub constants who are used in the same crate. Type annotation should only be required when the pub constant can't be inferred because it's only exported, not used in the same crate. I use pub for a lot of constants (like those above) that are only used in the same crate, in neighboring modules (they could be pub(crate) but that's more to type so I usually don't do it, unless I publish a crate and want to hide them) and I would want inference to work for them, too.

@DoumanAsh
Copy link

Is there per se anything that stops us from having type inference for static/const regardless of context?
And why we should treat const/static differently from mut

@Ralith
Copy link

Ralith commented Nov 25, 2019

I'd like this to enable consts containing anonymous types, which are currently impossible.

@Stargateur
Copy link

@Ralith what do you mean ?

@shepmaster
Copy link
Member

I'd like this to enable consts containing anonymous types, which are currently impossible.

That's not required to support this case:

// 1.41.0-nightly (2019-11-24 412f43ac5b4ae8c3599e)
#![feature(impl_trait_in_bindings)]

const VERSION: impl std::fmt::Display = 42;

fn main() {
    println!("{}", VERSION);
}

(Playground)

Output:

42

Errors:

   Compiling playground v0.0.1 (/playground)
warning: the feature `impl_trait_in_bindings` is incomplete and may cause the compiler to crash
 --> src/main.rs:2:12
  |
2 | #![feature(impl_trait_in_bindings)]
  |            ^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: `#[warn(incomplete_features)]` on by default

    Finished dev [unoptimized + debuginfo] target(s) in 0.87s
     Running `target/debug/playground`

@Ralith
Copy link

Ralith commented Nov 25, 2019

Oh, nice! If that's on the path to stabilization then it's indeed a better solution for me.

@matklad
Copy link
Member

matklad commented Feb 4, 2020

One thing that is missing from the discussion so far is that sometimes types are unnameable.

For example, for once_cell RFC we want to write

statict DATA: Lazy<String, _> = Lazy::new(|| "hello world".to_string());

but there's no way to spell the type of the closure. More generally, -> impl Trait is impossible to use for a static.

@matklad
Copy link
Member

matklad commented Feb 4, 2020

As for the general ergonomics argument, I have data that not having type explicitly specified is annoying for ides, as it prevents an ide from ignoring the body of static/const.

I also have a personal opinion that statics and consts are comparatively rare constructs, so ergonomics wins here are at best marginal. I am also annoyed by needing to specify the lenghts of the arrays in statics, but this happens extremely rarely and, well, having a length in there is useful when reading the code, as you don't have to count lines manually to get a feel for how large is the thing.

@shepmaster
Copy link
Member

shepmaster commented Feb 4, 2020

More generally, -> impl Trait is impossible to use for a static.

I don't follow why my comment almost immediately above yours does not apply:

#![feature(impl_trait_in_bindings)]

use once_cell::sync::Lazy;

static DATA: Lazy<String, impl FnOnce() -> String> = Lazy::new(|| "hello world".to_string());

fn main() {
    println!("Hello, world!");
}
warning: the feature `impl_trait_in_bindings` is incomplete and may cause the compiler to crash
 --> src/main.rs:1:12
  |
1 | #![feature(impl_trait_in_bindings)]
  |            ^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: `#[warn(incomplete_features)]` on by default

warning: static item is never used: `DATA`
 --> src/main.rs:5:1
  |
5 | static DATA: Lazy<String, impl FnOnce() -> String> = Lazy::new(|| "hello world".to_string());
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: `#[warn(dead_code)]` on by default

    Finished dev [unoptimized + debuginfo] target(s) in 0.01s

Perhaps you could expand on exactly what the problem is?

@matklad
Copy link
Member

matklad commented Feb 4, 2020

@shepmaster argh, sorry, I've read the thread here from the start, but apparently I've missed all the comments after #1349 (comment) somehow =/

So yeah, sorry for repeating what was already said: #1349 (comment)

And yeah, allowing imp Trait in constants fixes that.

@veryjos
Copy link

veryjos commented Jan 1, 2022

I just noticed that #2010 is not linked to anywhere on this issue, which is pretty weird since that appears to be the last big discussion where new things were learned and problems raised that led to the current stalemate. For future readers, the biggest surprise was summarised by niko as follows:

based on the data that @schuster gathered, it looks like enabling i32 fallback would basically always pick the wrong type for simple things like const FOO = 22. This means you would still have to annotate cases of simple integer literals, and yet those appears to be the vast majority of constants!

I think this might just be a self-fulfilling prophecy, in the sense that you're only going to find simple consts because any complicated type is too annoying to type out. That's how I ended up in this thread, and what I ended up doing, anyway.

@Kinrany
Copy link

Kinrany commented Jan 25, 2023

One pattern that I briefly considered but probably wouldn't use due to annoyance is having compile-time code that operates on modules with const values and functions in them:

mod cat {
  const sound = "meow";
  const legs = 4u32;
}
mod snake {
  const sound = "hiss";
  const legs = 0u32;
}

This is similar to non-Turing-complete configuration languages like CUE and Dhall.

It may be preferable to use some form of traits instead, but there is at least one well-known example of modules being used like this: serde data format conventions.

@MarByteBeep
Copy link

I just want to give the perspective from a new user. I'm just experimenting with Rust and ran into this problem. When reading the docs I thought, why are they using let here? Wouldn't it be better to use const, since those months are constants. So I tried this:

fn main() {
    const MONTH = ["Jan", "Feb"];
    println!("{}", MONTHS[0]);
}

Which gave me the following error:

help: provide a type for the constant: `: [&str; 2]`

So I fixed it like this:

fn main() {
    const MONTHS: [&str; 2] = ["Jan", "Feb"];
    let months = ["Jan", "Feb"];
    println!("{} {}", MONTHS[0], months[1]);
}

But in all honesty, from a consistency perspective for me it doesn't make any sense as to why I need to explicitly specify the type (string reference) and the size, while both can be inferred perfectly, just as the case with let demonstrates in this exact same example.

I'm sure there are valid reasons for this, but for a newbie like me it's just something that feels like an inconsistency.

@TeamPuzel
Copy link

This is extremely annoying with include_bytes!

pub const SOME_IMAGE: [u8; 2048] = include_bytes!("someimage.png");

When the image changes you need to manually change the length... The experimental feature to infer this also doesn't work for constants.

@ChayimFriedman2
Copy link

@TeamPuzel You can use:

pub const SOME_IMAGE: [u8; include_bytes!("someimage.png").len()] = *include_bytes!("someimage.png");

@TeamPuzel
Copy link

@ChayimFriedman2 Thanks, it's certainly less painful than constantly fixing my constants :)
Still, that's extremely verbose. Inference would make this a lot better.

@Neo-Zhixing
Copy link

Just to add onto this discussion, having type inference for consts make it much easier to implement certain macros.

@daniel-pfeiffer
Copy link

daniel-pfeiffer commented Dec 31, 2023

Good that we have an RFC! 😊 Redundancy is less of a technical issue in Rust, because the compiler will catch deviations. But I'm annoyed by the look and that it feels often feels redundant.

My use case is in actix_web, where I can declare the route with #[get("/some/uri")] async fn … but sadly that doesn't activate it. For that I must pass all my REST APIs (sub-tuples not needed, but nicer) in a different place. I find it wrong to repeat the names elsewhere:

actix_web_app.service(((a::f1, a::f2),
                       (b::f1, b::f2, b::f3)));

So instead, in each module (with of course much longer function names, each of which identically names its own type) I verbosely do:

pub const APIS: (f1, f2, f3) = (f1, f2, f3);

and (still needing to repeat the mod names):

actix_web_app.service((a::APIS, b::APIS));

@Miezhiko
Copy link

Miezhiko commented Jun 6, 2024

currently to define const structs I have to write

const A: B = B {

even if it's nice maybe to have type specified here as language syntax construction I have this B = B thing and in my opinion this looks confusing and weird specially when you have to define different structure you have always those B = B things

in haskell we write

a :: B
a = B 1 2 3

but

  1. type line is optional, yes maybe you will have warning if you don't add it but you can suppress it for code readability when you need it
  2. by syntax it's a :: B and a = B and not B = B which is big difference for readability

and in C++ it will be

const B a = { ... };

which actually is good syntax sugar too, we provide the type but deduce the constructor we want to use, so maybe at least const A: B = {, which is actually some trick to existing syntax but still improves readability imho

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
T-lang Relevant to the language team, which will review and decide on the RFC.
Projects
None yet
Development

No branches or pull requests