Idea: In the next edition, stop accepting `0.` as a valid float literal

On reading this post just now, it occurred to me this is exactly the kind of breaking change editions are designed for: purely surface syntax, zero semantic implications, 0. -> 0.0 seems almost trivial to lint and rustfix, etc.

What does everyone else think? Have I missed some glaring reason why it's not so trivial? Or is the potential gain too small to seriously consider this at all?

30 Likes

One notable gain is that we can use the . operator in more places because it won't conflict with the floating point syntax. Since the lang team seems to be moving in the direction of overloading the . operator (esp. with the new future.await syntax), this change would make things better on that front.

12 Likes

(Note: speaking for myself, not for the language team, though I'd be happy to support this in the language team.)

I'd love to see an RFC for this, and would happily support it.

I'd like to see it introduced initially as an off-by-default lint in the current version, then a crater run with that lint turned on to see how widespread it is, and finally changed to warn-by-default if that seems reasonable. Rust 2021 could make it a hard error.

(Even if the crater run determines that it's too widespread to change, I'd still like to see the off-by-default lint available for people who would like to enforce that style.)

7 Likes

Python requires parentheses around numbers to call methods on them (55).bit_length(), 55.bit_length() is a syntax error. So that is the (in my opinion not terrible) easy alternative. We are also not going to have a direct way to resolve this for open-ended ranges, as I don't think I ever want 0....contains(55.) to compile, nor 0.0...contains(55.0).

I am not opposed to this proposal wholesale; I don't think it is a dire problem to fix.

3 Likes

Except in the most annoying case of tuple_tuple_struct.0.0 which still won't be legal.

2 Likes

I think we could make that legal, by making it mean the same thing as (tuple_tuple_struct.0).0 but that is a different discussion.


link to other thread that I just made for this:

I like being able to wrxte "0." To mean float literal. I'd hate to have to write "0.0" and be forced to use 3 chars for something that only needs 1. I'd rather be able to get away with 0 and let type inference work out the rest.

syntax highlighting makes it clear it's.a number. so the readability isn't a problem for me at least. and since we often put them in vec![1.0,2.0,3.0] they can get long and awkward faster with more wasted chars

1 Like

Rust doesn't allow coersion from an integer type to a floating point type for reasonable reasons.

But is there a reason why we don't allow integer literals to be inferred to be a floating point type?

It might make sense to allow {integer} to become f__. If it's worry about implicitly accepting the lower precision of floating point types, we can introduce a lint like we already have for oversized integer literals for integer types.

7 Likes

I don't like that Rust doesn't accept float > 0 or let x: f32 = 0. This gets even more annoying for float tuples that can't be (0,0). So the shorter syntax makes it barely-tolerable (0.,0.), and to me the I'm-seeing-double syntax (0.0,0.0) would be just too much.

11 Likes

FWIW, I am also a fan of expanding type inference on literals. A long time ago I proposed making let x: String = "abc"; compile, and I think making let x: f32 = 0; compile is also a good idea for the same basic reason: It's extremely obvious what the type has to be, so forcing the user to go through a compile error -> add to_string() / .0 -> rebuild cycle is just being pedantic in a way that "makes simple things hard" and primarily affects the sort of toy programs that impact a first-time user's early impressions of Rust. I would also expect let x: f32 = 0; to be less controversial because it doesn't involve type inference changing a stack variable into a heap allocation.

A lot of that old thread got sidetracked by much broader and/or more magical proposals that never really had a chance, but AFAIK the only serious problem with these ideas back then was:

and my impression is that even today, this is still not a fully solved problem in chalk/rustc.


To move the discussion forward: assume for now that this is solvable, and we live in the happy timeline where someday it will be implementable in rustc with "no risk". Would anyone object to making let x: f32 = 0; compile, i.e. allowing numeric literals to infer to floating point types as well as integer types?

If everyone is neutral or in favor of that (as I currently suspect we are), then I think it makes perfect sense to postpone any changes to 0. until after we've made let x: f32 = 0; compile.

14 Likes

In Python parentheses are not required. A white space is enough. This is valid both in Python2 and Python3:

55 .bit_length()

And in Rust, too, you can use white space as separator. This is valid:

5. ..7.

3 Likes

Allowing an {integer} literal where a float is expected would break some code, e.g.

trait Foo {}
impl Foo for i64 {}
impl Foo for f64 {}

fn foo(_foo: impl Foo) {}

fn main() {
    // works, because Foo is implemented for only 1 {integer} type
    foo(5);
}

But maybe Rustfix could solve this?

2 Likes

I was imagining this would be like today's integer fallback, where i32 is the "default" used whenever there's a conflict. Of course we'd need a slightly more complex defaulting scheme when more types are allowed, but presumably we'd make it choose integer over floating point when both work.

4 Likes

Huh... apparently our existing number literal type inference is a lot dumber than I thought it was. Even this doesn't work:

trait Foo {}
impl Foo for i16 {}
impl Foo for i64 {}

fn foo(_foo: impl Foo) {}

fn main() {
    // the trait `Foo` is not implemented for `i32`
    foo(5);
}

I guess we'll have to add this to the pile of "figure out how to properly implement these fallbacks",

Yes, very much so. I'd like to distinguish between integer literals and floating-point literals.

8 Likes

I kind of concur with that. There is a big semantic difference between exact arithmetic (integers) and approximate arithmetic (floating point). Using floating-point constants, preferably with a digit on each side of the decimal point, makes it obvious to the reader that a floating-point value is involved. Since code is read many more times than it's written, and usually by many more people, the bias should be toward reader understanding rather than minimizing the writer's keystrokes.

8 Likes

Please keep the omitted zeros feature. I'm in favour of them, I think being able to use 1., 2., 3. in numerical code is a good feature that I'm using all the time. Less typing in combination with being easier to read. You might have many numbers to enter in parameters, tests or constants!

A minor point, but it also improves clarity. 1., 2., 3., 4., 5. is easier to read than 1.0, 2.0, 3.0, 4.0, 5.0 because the zeros are visual noise that makes the text dense and harder to read at a glance.

A compromise would be if the syntax 1, 2, 3, 4, 5 was made available instead — that is writing floats with no decimal point at all.

5 Likes

One man’s noise is another man’s signal, I suppose.

I personally don’t have any preference. I always use .0 but I’ve never had problems with code that just uses .. All I want is a configurable cargo fix that would let me enforce one or the other automatically, depending on the project’s particular style choice (I don’t know if this already exists; I haven’t checked because it’s at the bottom of my priority list).

3 Likes

Yup, and that's essentially what's blocking allowing indexing by more types -- even one more unsigned type, and v[0] treats the 0 as i32.

2 Likes

I find 1., to be especially awkward to read, because of the adjacent . and ,. My mental lexer has an easier time with 1.0,.

But in any case, the proposal isn't to drop this because of readability, the proposal is to drop this to free up syntax for other purposes, because of the other use of . for method/field/etc access.

4 Likes