What are the downsides to having different data types for floats and integers instead of just a number data type? I know that having different ones can improve memory usage. But are there any downsides other than some programmers using the language may not like it?
It's not a common case, but integral data types (sometimes known as shorts and longs) allow for integer overflow, which may be used by clever programmers (if the programming language allows it). E.g. for an unsigned 8-bit integer type, with a range from 0 to 255, a programmer may use the fact that 255 + 1 = 0. Such use should be well-documented, and I can't remember the last time I've used it, but it's definitely possible and very difficult to support with floating point numbers.
The main advantage of having a number type is simplicity and ease of use. Many versions of BASIC just had floating point numbers, which reduced the size of the code (given that the first home computer, which Bill Gates wrote the BASIC for, had only 4K of memory, this was very important) and made it so that beginning programmers didn't have to learn the difference between integers and floats, everything just being numbers.
Code size is no longer an issue; few deal with systems with less than 1MB of memory. When writing a language for a beginning audience, especially children, I can still see the value in simplifying that detail.
The problem with having only a single number type is: which number type do you pick?
There are two problems with this. Everybody knows about the limitations of floating point with its fixed precision and accuracy, and that's what everybody is focusing on, but that's only half the story. There is another problem here: the fact that it is a binary representation.
I would argue that a programming language that does not have a good reason to do so (e.g. a language specifically for cryptography) should choose base-10 as the default number representation. I would also argue that arbitrary precision decimal rationals are a better default type than either floats or integers. I.e. make
12.34 syntactic sugar for
12.34% syntactic sugar for
Arbitrary precision decimal rationals will work as long as you restrict the operations to addition, subtraction, multiplication, and division, as well as exponentiation by a natural number. For most users, those should be enough. Heck, I would bet a good sum of money that a significant portion of users don't even know that an exponent can be negative or non-integer!
It gets more interesting when you get into geometry, though. You can't avoid roots and trigonometry here, so you will quickly run into irrational and transcendental numbers. Same for finance, where you will encounter logarithms at some point.
Here, you have two choices: turn your language into a CAS (with all the complexity that carries) or punt to some sort of inexact type. In the latter case, I would still argue it should be base-10, and possibly also bigger than just 64 bit.
One idea might be to keep function applications around as un-evaluated expressions. For example, if you encounter
a := sqrt(2), instead of evaluating it and having to punt to an inexact type, you keep it around as an un-evaluated expression. Then, when you later encounter
a**2, you can cancel out the two operations and end up with a genuine
2 instead of something that is close to, but not equal to
The problem with such representations is that they are essentially programs. With all the consequences. It doesn't take much and all of a sudden, simply checking whether two numbers are equal requires checking whether two programs compute the same function, which in the general case is equivalent to solving the Halting Problem. You end up with a number representation where your numbers are super-precise and super-accurate, but you cannot even tell whether they are equal.
For an example of a language that cares a lot about accurate and precise numbers, take a look at Frink.
In conclusion, I would say there should be a default type that is useful for a lot of real-world cases. It should be decimal, have arbitrary precision, and support at least rational numbers. (Of course, the in-memory representation can be optimized as required.) For operations that are not closed under the rationals, there should be alternatives with easy conversions all around.
Floating point numbers can often have accuracy issues when a number is too small or too big, see relevant stackoverflow.
Ofttimes, you have a function that takes a float, but you only have integers. Especially with languages where number conversions are explicit, this can become quite verbose. For example, Kotlin has no power operator for
Ints, just for
Floats. I have written the following code at least three times by now:
val x = y.toDouble().pow(z.toDouble()).toInt()