Why doesn’t Java/Kotlin support Int.NEGATIVE_INFINITY
and Int.POSITIVE_INFINITY
?
Currently, we need to convert to Double to use infinity numbers in case Int.MIN_VALUE
and Int.MAX_VALUE
are not accepted.
An integer is coded by four bytes. All bits combinations of those four bytes do correspond to an integer. How would you code infinities in this context?
Maybe I would like to restate my question.
How do we present Double
infinity? And why can’t we present Int
infinity in a similar way?
Integers and floating point numbers are fundamentally different types of numbers in computer science. They have different properties, are used for different things and are internally stored in a much different way.
Integers are fully precise, but to accomplish this, they can only use a relatively small range of numbers focused around zero. They’re good for discrete mathematics, for things like your bank account number, amount on this account, number of bank clients, etc. Floats are imprecise, but can represent almost any number - very, very small or very, very large, including infinities. They are associated with real numbers and are good for e.g. physical values like distances, temperature, etc.
Integers could potentially support positive and negative infinity, but that would be pretty much useless. When using integers we work in this small range they support and we are expected to not exceed this range, so there is no point in suddenly jumping to infinity. Also, infinity doesn’t make sense for things like: account number, its amount, etc.
To add to what broot says:
The fundamental answer is ‘because that’s how we define them’.
Why are they defined that way? Probably for reasons of history, simplicity, and usefulness. Integers are precise and have a very well-defined, clear meaning; there’s no need to waste bit-patterns or circuitry/cycles on non-numbers. Floating-point numbers are inherently ‘fuzzy’, and having not-really-numbers available can help work around some of their issues; and they’re already fairly complex, so it doesn’t significantly increase their complexity.
Long answer:
A pattern of bits could represent anything: an image on screen, a genetic code… To give it a meaning, we treat it as a type.
Integer types are fairly simple: we treat a value as one of a consecutive range of integers, centred around zero (for signed integers), or starting from zero (for unsigned). For example, a single byte could be treated as a number in the range -128 to 127, or 0 to 255. Processing these is fairly straightforward: the circuitry to add two integers is pretty simple, and the only real problems you get is overflow (when the result of a calculation is outside the available range) — which can be easily detected and signalled by a simple flag — and division by zero (which can be easily prevented in software).
Now, we could treat one of those values specially, and call it ‘infinity’, but we don’t. Treating values differently in hardware would need a lot more circuitry, while doing it in software would take many more processing cycles each time. Since there’s never been any real need for infinity when processing integers — after all, no other common integer arithmetic operations result in infinity — it’s never been done.
Floating-point types are much more complex. They generally store as a mantissa (a binary fraction between ½ and 1), a signed exponent (a number of digits to shift the binary point right or left), and a bit for the sign. (Different floating-point specifications assign different numbers of bits to the mantissa and exponent, but they generally work in a similar way.) Adding, multiplying, and other operations involve far more operations (which is why they were developed later).
They’re also different conceptually: we like to think of them as real numbers, but of course those form a continuum that can’t be stored precisely in any digital system, and so floating-point can only store a discrete set of real numbers (which are hopefully near enough to the real numbers we want). This brings an inherent ‘fuzziness’ — there’s almost always a gap between the real number we want to store, and the nearest floating-point value to it.
(Aside: it doesn’t help that we tend to think in decimal fractions, but floating-point types usually store binary fractions. And because most decimal fractions form non-terminating binary fractions — e.g 0.1₁₀ = 0.00011001100110011…₂ — so most decimal fractions can’t be stored exactly in floating-point types. This brings a lot of confusion — especially because when displaying floating-point numbers, most systems try to round to the nearest decimal fraction, which sometimes hides the issue, making it even more surprising when that doesn’t work.)
Because of this, they bring many more issues: for example, as well as overflow, you can get rounding errors (when the floating-point number isn’t as close as you need to the real number you want), underflow (when the result is too small to store), and loss of precision (AKA denormalisation, e.g. when subtracting almost-equal two numbers, most of the mantissa bits cancel out, leaving very little precision remaining). Compared with integers, there are many more standard operations on floating-point values, such as exponentiation, trigonometric and logarithmic functions, and so on — and so many more opportunities for these issues to arise.
In order to work around these issues a little, most floating-point types reserve certain values to represent special values: a negative 0 (that’s distinct from the normal zero), a positive zero, a positive infinity, a negative infinity, and/or a value to represent ‘not a number’. These allow many calculations to return slightly-meaningful values instead of stopping with an error, and in some cases can allow slightly-meaningful results from a chain of calculations. (For example, dividing a negative number by zero could yield negative infinity.)
This is feasible because calculations involving floating-point numbers are already pretty complex, and so handling these cases doesn’t increase the complexity by very much. Also, floating-point numbers generally take many more bits than most integer types, and so reserving a handful of bit patterns isn’t a significant loss. And these not-really-a-number values fit well with the existing ‘fuzziness’ of floating-point numbers (compared to the real numbers we want).
So the trade-off generally works against them for integer types, but in their favour for floating-point types. And so that’s how those are usually defined.
(Sorry my previous post grew to be so much longer than expected )
There’s also a Kotlin-specific answer: because that’s how the system types work on the JVM — and in JavaScript, and in fact all common platforms that Kotlin runs on. Which in turn is because that’s what common hardware supports. (Which is for the reasons above.)
Thanks @broot, @gidds for your explanations! That’s exactly the answers I want (both short and long answers ).
Is there a standard type that abstracts away the subclass of Number and includes a unit?
Most real-world calculations involve a unit, and it’s a pretty broad class of error (like null safety) to multiply the wrong two numbers when, for eg: one is measured in meters and the other, yards. I would expect a comprehensive solution to this class of error that every programmer shouldn’t reinvent for themselves.
Other errors and common needs such a class might also address:
- abstract away the binary form of the Number and allow people to multiply ints with doubles, and
- include an infinity singleton, and
- guard against integer overflow, and
- silently convert yards to kilometers or light years or whatever, and
- handle composition of units like G = 9.8 m/s^2 (“meters per second per second” is the unit for acceleration), and
- serialize and decode predictably
If I knew of such a library, I would recommend it in this thread. Hoping someone else will.
I can’t recommend any library that would provide existing real-world units with conversions, etc., but the problem itself could be solved by using value classes, for example:
@JvmInline
value class Distance(val meters: Double)
@JvmInline
value class TemperatureCelsius(val celsius: Double)
@JvmInline
value class TemperatureKelvin(val kelvin: Double)
This way you can’t mistakenly mix values of different types or units, you can add your own behavior or conversions, you can provide overflow detection, etc. Internally they are still represented as just primitives - at least most of the time.
Maybe you’re looking for something like this?:
or