Why is there no `Number.minOrNull()` ?
# stdlib
h
Why is there no
Number.minOrNull()
?
d
Isn't that impossible to implement?
h
Aren't numbers comparable?
d
You're thinking of this signature right?
fun Number.minOrNull(): Number
h
Uups, I meant
List<Number>.minOrNull()
I guess the problem is that
Number
is not comparable in kotlin. Why would that be?
d
That I don't have an answer for 😅 .
It looks like each subclass implements Comparable actually.
h
Yep, but somehow it did not end up in Number.
d
Ahhhhh I got it. Complex numbers can't be compared but they are still numbers.
Not all numbers are comparable, that's why.
h
Why on earth did they squeeze those into the Number type. I get their application in physics but it's IMHO a rare edge use-case.
d
Maybe there should be a
ComparableNumber
.
Yes it's rare but for something that goes into the stdlib, it should cover everything.
h
Or
RealNumber
3
d
Also, with generics, you can simply ask for a
T : Number, Comparable<T>
I guess.
Why do you need this function btw?
h
I have
List<Number>
and want to know its minimum. I know I can map them, but I was just curious why I have to.
d
Why do you have a
List<Number>
, why not a more specific type?
I've never seen
Number
used in practice.
h
I thought it to be more memory efficient in large collections. E.g. forcing a million Ints to into double will require more memory.
But clearly if we/I can't do operations (like comparisons) it not really useful.
e
Number is almost never useful in Java (where it came from) nor Kotlin (which inherited it)
☝️ 1
for what it's worth, you're saving almost nothing with a List<Number> filled with Int vs List<Double> - the overhead from the boxing required for generics is much larger than the 32-bit Int vs 64-bit Double
☝️ 1
for example, on 64-bit JVMs, the object header is 12 bytes, and alignment is 8 bytes, so an Integer takes 16 bytes of memory and a Double takes 24, and they are located in the heap so there is additional overhead in tracking that
if you are in a situation where memory really matters, you have to use IntAtrray or DoubleArray which do not box. if you can't keep your ints and doubles separate, then you might as well just use Double
6
h
Many thanks @ephemient for the clarification and the great advice.
n
Coming late to the party, but felt like dropping in a comment. As I understand it, Java's
Number
is mostly about having common OO interface for transforming numbers between different representations, but it doesn't want to force a possibly unwanted way of comparing the values, like our brain does it: Comparing
1
and
2
(ints) is easy, but for
1
and
1.1
(int, float) our brain automatically goes into floating points mode and is able to do the comparison, but for the computer system, it first needs to convert one or the other into a binary pattern (where integers and floating points have completely different layout) to be able to do a comparison. I'm assuming the Java team never wanted to force an implicit, possibly resource intensive conversion for comparisons, thus leaving it out. I have some maths libraries developed, where every outside-facing API method receiving data has a signature with
Number
type, but internally everything is
IntArray
or
DoubleArray
base on the wrapper's type and converted to/from at the boundaries, thus offering the stats operations with an explicit possibility to use the "least precise" type if double precision isn't needed
e
there isn't a good way to compare numbers of different types without losing precision
e.g. conversion between Long and Double is lossy in both directions, and the only larger type that can accurately represent both (ignoring infinity/NaN) is BigDecimal
n
Correct. And I just noticed I forgot to mention the explicit context (I want integer precision or floating point precision) which is thus left to developer to explicitly implement, if/when comparison is done
e
also it sounds like you're only accepting existing known subtypes of Number? there's no reason a user couldn't implement
Copy code
data class Int128(val high: Long, val low: Long) : Number() { ... }
and unless your code knows about the type, there's not really anything it can do with it
although that's a problem in Java already; built-in APIs like NumberFormat accept Number but will fail on any non-builtin subtypes
but in any case, I generally consider use of
Number
to be a code smell. it isn't possible to express which subtypes of number the code can actually work with, and there's no way to generically handle all types of numbers
n
Yes, I assume we agree the
Number
type (in Java and Kotlin) are a bit weird if they are viewed as if it represented a number like in real world. When it comes to (my) libraries supporting custom number types, the general contract holds: any
Number
subtype must implement
toInt, toDouble
etc and my library will handle its value accordingly, given which ever wrapper type context is chosen by the user
e
so that leaves out double-double, unum, or other types that can reasonably extend
Number
but will always have lossy `toInt`/`toDouble`? at that point I would just wire up overloads for all the primitive numeric types, since that's all you're going to be able to work with.
n
Again, it's about context. With my libs, I don't support types larger than the stdlib offers, and I explicitly document it with the methods. So, if someone chose to input a type with more precision that is offered by the calculations, it's on them. Like with many things with Kotlin, it is about convenience, and about consise code: inbound type being
T : Number
will rid the user of doing an additional
to*
method calls as I'm able to do it for them, to fit the documented, internal representation of the data. I not sure how any of this contradicts on the issue we both agree upon:
Number
doesn't implement
Comparable
to other number subtypes, as it wouldn't make sense in common terms, without the developers' input about the context in which the comparison would be done
e
if you provide
f(Byte) f(UByte) f(Short) f(UShort) f(Int) f(UInt) f(Long) f(ULong) f(Float) f(Double)
then the user is similarly able to use your
f
regardless of which numeric type they have (and in fact they're in an even better position, because
UByte UShort UInt ULong
do not extend
Number
), while also declaring, at the type level, that
BigInteger
needs to be converted.
h
Sure, but that's already a big api to maintain. And when adding the combinatorics of f(Number, Number), it quickly grows insane to include/maintain/document all the signature variations.
e
stdlib uses code generation (e.g. the collection methods on Iterable, ByteArray, CharArray, ShortArray, IntArray, LongArray, FloatArray, DoubleArray), and even though it's more effort for the library author, it's a better experience for the user