Remove error "expected f64 found integral variable" (allow numeric literals without `.` where it's never ambiguous)


#1

Rust is very nitpicky about use of integer literals where floating point types are expected.

let x: (f64,f64) = (0,0); // Not allowed

let x: (f64,f64) = (0.0,0.0); // Not as readable

Similarly:

if y > 0 // not allowed for f64 :( 

error: mismatched types: expected f64, found _ (expected f64, found integral variable)

I don’t see any harm in allowing implicit conversion from integral literal to floating point type if the value fits without loss of precision. I think it would allow avoiding unnecessary noise in the code and save me from frequent compilation errors where I forget a “decorative” .0.

The abundance of .0 also makes code less clear when working with tuples: 1.0 + p.0


BTW: the widening discussion doesn’t seem to cover integer > float conversions. Should it?


#2

I totally agree, but I will help you until we’re there: drop the last 0, and enter float literals as 0., 1., 2. etc.!


#3

That helps thanks (I did find’n’replace of .0 and broke all my tuples :smile:)


#4

I agree this isn’t very readable, but with some spaces:

let x: (f64, f64) = (0.0, 0.0);

I personally find it very visually easy to parse. You can also do:

let x: (f64, f64) = (0., 0.);

But I think that’s worse.


#5

-1 Rust just enforces a popular (and, IMO, perfectly reasonable) guideline. (See http://google-styleguide.googlecode.com/svn/trunk/cppguide.html#0_and_nullptr/NULL for example)

And as @steveklabnik mentioned, you artificially worsen the readability by not using the spaces.


#6

I don’t think C(++) is a good reference here, because in C implicit type coercion is lax and thus not trusted. What I’m proposing here is very limited, so it doesn’t require C level of paranoia.


#7

It’s less bad with spaces, but I prefer to use (lack of) spaces to group things, e.g.

add_points((0,0), (1,1));

rather than

add_points((0., 0.), (1., 1.));
add_points((0.0, 0.0), (1.0, 1.0));

and I don’t use .0, because it’s horrible when also using tuples.


#8

I think it’s a bad coding style and hence should not be encouraged by allowing auto casting, especially because of the type inference :

let x = (0, 0);

Did you mean float or int here ? Obviously, in some case you are required to write the plain type, i know type suffixes will solve that, but i’m not sure if allowing casting in some case is a good practice to adopt.


#9

Joris, this isn’t a case I’m advocating for.

I mean relaxing specific cases where required type is already known, so there is no ambiguity, and the only thing preventing it from working correctly is a deliberate error condition:

(expected f64, found integral variable)


#10

There might be ambiguity for the reader though. Of course, the compiler can figure it out, but can the reader of the code?


#11

I think the answer to that question is a resounding “yes”.

When I personally am using integer or floating point literals, I already have a pretty solid context. I’m not thinking of how many bits it makes up or if it’s signed or not or if it’s integral or floating, I’m thinking of it as a good, old-fashioned decimal. And if this trivial change saves me from having to find everywhere I used 1 instead of 1.0 when their meanings are semantically and numerically equivalent, then I +1 the hell out of it.

Rust has enough common compiler errors for the user to deal with. No one should have to waste their time correcting purely pedantic errors like this one.


#12

“Purely pedantic” errors help writing unambiguous code, like not allowing string + integer although it has very clear semantics (append the integer as decimal number).


#13

But is let float: f32 = 1; at all ambiguous? You’d type that without thinking; I do it all the time. It makes perfect sense to me, and works fine in other languages. Thus why I think this change is necessary and the issue pedantic.

I cannot think of one single instance where using an integer literal to represent a floating-point would create a logic error. The compiler would error on any mismatch or real ambiguity, and there’s no information loss because this change doesn’t add the ability to assign a float literal as an integral value.


#14

Your assignment does indeed not look ambiguous. However, unlike in your code, the constraint to f32 might appear much later in the code.


#15

@tbu, but that has nothing to do with the feature I’m proposing.

I’m not proposing changes to type inference. I’m not proposing C-like type conversions that cause loss of precision and unexpected behavior. I’m not proposing allowing adding strings to integers!

I’m proposing to remove a very specific error that happens in a very unambiguous case where the required type has already been inferred, so it’s absolutely unambiguous that you have an integer literal used in context where floating point type is expected, and it’s possible to guarantee that the result will be exactly identical as if you added . to the literal.


#16

I think I did understand you and wrote the above comment with that in mind. A constraint to f32 means that the type inference can decide that the literal needs to be a f32. I still wouldn’t call that “absolutely unambiguous”.

See e.g.:

 let foo = if bar == baz {
     (0..1000).map(|_| 'a').collect()
 } else {
     [...]
     format!("Yay!")
 }

I recently stumbled upon such a code fragment, imagine there’s lots of code instead of the [...]. Although it’s entirely obvious to the compiler that the code in the first if branch must return a String, I would initially assume it’s a Vec<char> because I didn’t see any constraints and Vec<T> is what collect defaults to. I can imagine that similarly such a float inference is non-obvious.

EDIT: The bold un above.


#17

@tbu I wrote “absolutely unambiguous”. I’m not sure if you made a typo there or are we talking past each other.


#18

Also please note that in situation where there is no explicit type constraint to f32/f64 my proposal simply does not apply, because the error “expected f64, found integral variable” won’t happen.


#19

Fixed.

I understand that. In my post I describe exactly this situation and how it can lead to ambiguities for the reader.


#20

I think there’s still a miscommunication here.

Let me ask a question, starting with a modified version of @tbu’s example:

fn main() {
    let bar = true;
    let baz = true;
    let foo = if bar == baz {
        (0..10).map(|_| 1).collect() // line 5
    } else {
        println!("Else statement!");
        let v : Vec<f32> = [...];    // line 8
        [...]
        v
    };
    println!("Foo: {:?}", foo);
}

Does this count as “completely unambiguous”? To the compiler, its clearly unambiguous: foo is definitely a Vec<f32>, so on line 5, we must be talking about f32s.

It sounds to me like you’re saying that that doesn’t count as “completely unambiguous”, that the only thing that counts as “completely unambiguous” is to have the type hint on the same line. However, I don’t think there are any places in the compiler that treat “on the same line” as any more or less ambiguous than the example above; are you suggesting that we special-case this, just for float literals?