Tidy floating point literals

Taking a look at code like this:

I wonder if it’s a good idea to accept floating point literals like “0.” instead of requiring a digit after the point too, like “0.0” (unfortunately a breaking change).

It’s a lot less typing and it’s easy to prefer it that way.

I actually think it is easier to read when writing it as short as possible, and thus I claim that the 0. version is more tidy.

array![[0., 1., 2.],
       [3., 4., 5.]]

array![[0.0, 1.0, 2.0],
       [3.0, 4.0, 5.0]]

Of course Rust could learn to accept integer literals as floats and it would be even tidier IMO.

array![[0, 1, 2],
       [3, 4, 5]]

(relying on type inference is not exactly uncommon in Rust, so why not.)

4 Likes

It would be even better to go in the direction of Golang, where numeric literals, expressions involving them, and even named constants, can be typeless numbers.

In a code like const FIVE = 5, assigning any specific type to the constant FIVE is completely useless. You can’t take the address of it, so what’s the point?

We still have to type-check constants and assign a type to them, unless you only want to allow literally just literals which seems way less useful than arbitrary constant expressions.

See https://github.com/rust-lang/rfcs/pull/1945.

Internally you could consider such constants to have a single-value type representing the real number that the literal represents. Such type is trivially coercible to any machine type that can represent it. Type-checking of expressions would even do constant folding as a side effect.

And it’s not like we’d be adding something entirely new to the language, or the compiler. We already have support for zero-sized types, () chief among them. Considering constants to have zero-sized types of their value would work almost as is.

I’m opposed. I don’t see any reason to complicate the grammar just to remove a single character per literal. This also increases the potential for ambiguity and confusing compile errors when you make a typo. Plus, having the .0 makes it easier to scan the code at a glance.

As for untyped constants, I think it’s a worthwhile idea, but there’s a lot of practical issues you’d have to work out. Maybe we could wait to see how the const fn stuff turns out first.

This made me realize that permitting .0 as a floating point literal would break code or lead to ambiguity which we have to resolve somehow: Currently, tuple.0 is three tokens IDENT(tuple) DOT INT(0), but with .0 being a token of its own the maximal munch rule would give us IDENT(tuple) FLOAT(0.0). To avoid breaking all field accesses on tuple and tuple structs, we'd have to introduce weird special cases which we currently avoid for similar issues such as tuple.0.0.

1 Like

tuple.0 is one of the few design mistakes of Rust: it introduces a trap, breaks a convention of Python/D and other languages, and makes it harder to use tuples inside a “static for”, just to save just one char… tuple[0][0] avoids those three problems.

1 Like

Even if that was true, it exists now and can’t be removed or broken. But I’m not at all convinced that’s the case: tuple[0] has other problems, chiefly that it can’t be a normal std::ops::Index impl (at least without introducing a ton of additional type system features that we don’t have and may never need).

I agree that probably it can’t be a normal std::ops::Index. Regarding “features that we don’t have and may never need”, take a look at C++17 and compare it with C++98: if a language enjoys lot of success you can’t easily tell what people will want to add to it 20-30 years from now. It’s better to design a language that can evolve. tuple.0 hurts that a little. While D language is full of design mistakes, it also contains several ideas worth copying.

How would one represent a “real number”?

Not an arbitrary real number, obviously. Just the subset of ℝ that can be expressed as a compile-time constant, which is also a strict subset of ℚ, but that just complicates things. The point is that it represents an abstract value of a number, in contrast to Rust numbers where 5u8, 5i32 and 5.0 are all different values, despite encoding the same abstract number.

In Go, untyped constants are represented by arbitrary** precision rationals that behave like Q until downcasting to concrete types at compile time. (The type conversion rules for assigning constants to concrete types are much more lenient than assignment between concrete types.) See the last section of the blog post on Constants.

This allows you to do math with very precise constants without losing fidelity before assigning the value to a concretely typed variable. E.g. (math.Pi * 1e1000) / 1e1000 is equal to math.Pi.

This is really nice for defining your own constants. E.g. if you want Tau = 2 * Pi, you can just do const Tau = 2 * math.Pi, then assign Tau to a float64 without worrying that you might have lost a few bits of precision (and precomputing it somewhere else to embed the literal separately to get the bits back). You can see this in the constants on the math package where e.g. Log2E = 1 / Ln2 and Ln2 is a defined as a 64-decimal-digit constant.

** I can’t seem to find the source right now but I believe the compiler team recently-ish put (still very big) limits on this to keep memory consumption during compilation from getting out of hand.

0 is a field, with field rules, so should look like it: rfcs/text/1506-adt-kinds.md at master · rust-lang/rfcs · GitHub. Today it's visible why I can (&mut tuple.0, &mut tuple.1) but not [&mut array[0], &mut array[1]]; if they were the same syntax I think that would be a bigger trap.

2 Likes

Why is (&mut tuple.0, &mut tuple.1) acceptable but not [&mut array[0], &mut array[1]]? In both cases the indexes are statically known and distinct...

The hurdle to clear is to know if its indexing maps distinct indices to nonoverlapping elements.

1 Like

At the end of the day, indexing is still an overloadable operator, while field access isn’t. Treating arrays and slices specially would only make difference in some cases, and introduce a big and unexpected inconsistency with std::vec and other library-defined array types.

1 Like

You can invent ways to overload field access too, if you really want to, in D language there's opDispatch:

But overloading field access is probably a niche usage.

True, but being able to do something doesn’t mean it should be done. D started out with the right ideas, but it ended up being just a different C++. By which I mean badly overengineered. In many things, it’s best for Rust to avoid being inspired by D.