Hi there. :wave: I recently wanted to implement a ...
# getting-started
d
Hi there. đź‘‹ I recently wanted to implement a generic interface with Comparable constraint for all numbers, but found that the Number class does not implement the Comparable interface. Are there any reasons for such behavior? All implementations (Double, Byte and etc) implements the Comparable interface (and can be compared to any of numbers except Char) and in general all numbers can be compared (and also used in basic math operations). Maybe it makes sense to make Char comparable with any number and add default Comparable implementation to Number or exclude Char from Number because it does not support basic numeric operations?
s
This question is probably as old as the
Comparable
interface itself. The tl;dr is that not all
Number
implementations can realistically be compared to each other, and that assuming desired behavior for comparison across disparate types that do not trivially have a total ordering is, at best, just plain frustrating for developers. For example, what should
1.compareTo(Float.NaN)
return? What if someone writes a complex number class? Do they just stub out
compareTo()
and hope nobody ever calls it? Every
Number
implementation would have to anticipate being compared to every other
Number
implementation in order to be able to convert to a comparable type (and ideally be smart enough to avoid losses in precision), which is a convention that’s 1) incredibly brittle and 2) really annoying lol. You could just throw if you run into a type that isn’t handled or comparable to the value you’re comparing to, but that would defeat the point and also be more type unsafe than the existing hierarchy. Much has been written on the topic, would recommend a read: https://stackoverflow.com/questions/480632/why-doesnt-java-lang-number-implement-comparable
d
Thank for the answer. Yes, this all is true, but There are already implemented almost all required compare functions to compare different types. And Int can be directly compared with Float. Code from Int class:
Copy code
public final operator fun compareTo(other: kotlin.Byte): <http://kotlin.Int|kotlin.Int> { /* compiled code */ }

public final operator fun compareTo(other: kotlin.Double): <http://kotlin.Int|kotlin.Int> { /* compiled code */ }

public final operator fun compareTo(other: kotlin.Float): <http://kotlin.Int|kotlin.Int> { /* compiled code */ }

public open operator fun compareTo(other: <http://kotlin.Int|kotlin.Int>): <http://kotlin.Int|kotlin.Int> { /* compiled code */ }

public final operator fun compareTo(other: kotlin.Long): <http://kotlin.Int|kotlin.Int> { /* compiled code */ }

public final operator fun compareTo(other: kotlin.Short): <http://kotlin.Int|kotlin.Int> { /* compiled code */ }
In case of Complex number it can be compared with all others numbers as a complex numbers because complex number is more generalized and there is no contradiction. If Number will be close to mathematical number hierarchy - all must be okay.
s
And Int can be directly compared with Float.
Right, the Kotlin devs have picked a strategy for comparisons between the two and implemented it. They deliberately chose to do so and did all the work to make it happen, which is very different from forcing everyone that extends Number to implement
compareTo(Number)
. Plus, this approach allows method overloading to pick the correct method to call instead of relying on runtime type checking. This is far more efficient and enables the type system to prevent someone from calling
compareTo
with a Number argument that Int doesn’t know how to handle by making it illegal to represent. Additionally, making a major change to the Number API would have complex ramifications for Kotlin/JVM because there’s a lot of platform type mapping there.
AtomicInteger
and the other number classes under
java.util.concurrent.atomic
all deliberately do not implement Comparable because they box values intended to be mutable, and something could update their values after being sorted. These classes implicitly now extend
kotlin.Number
because of Java compatibility, and making Number implement Comparable would mean adding
compareTo
to these classes, which is fraught with problems. Ultimately, allowing developers using the Number class to opt into implementing Comparable with disparate types is safer and more explicit, and helps avoid making assumptions that do not necessarily align with developers’ expectations.
d
Yes, you right there are a lot of side effects with adding
Comparable
to
Number
. I can propose other one. It possible to add class like:
Copy code
abstract class RealNumber <T : RealNumber<T>> : Number(), Comparable<T> {
    abstract operator fun minus(other: T): T
    abstract operator fun div(other: T): T
    abstract operator fun plus(other: T): T
    abstract operator fun inc(): T

    //maybe some others operators
}
And inherit from it
Float
and others (
Int
,
Byte
, etc., besides atomic) from a subset of real numbers. Such way:
Copy code
class Float : RealNumber<Float>() {
    override fun minus(other: Float): Float {}

    override fun div(other: Float): Float {}

    override fun plus(other: Float): Float {}
//...
It can add possibility to provide common logic of calculation and comparison for all numbers with minimum changes (
Float
and any others classes API does not change, types will be safe with minimum overhead). What specific problem am I facing (Example of problem that can be resolved) I have generic class like this:
Copy code
open class Interval <T>(private val start: T, private val end: T)
    : Comparable<ValueInterval<T>>
        where T: Comparable<T>, T: Any {

    fun isInside(value: T) : Boolean {
        return value > start && value <= end
    }

    override fun compareTo(other: ValueInterval<T>): Int {
        return this.start.compareTo(other.start)
    }
//others compare operations
}
I want to extend it with some number specific functions (use plus/minus etc). Currently it can be added only for each separate
Number
child (
Double
,
Int
). With using
RealNumber
it can be like this:
Copy code
class RealNumberInterval<T : RealNumber<T>> (
    private val start: T,
    private val end: T
) : ValueInterval<T>(start, end) {
    fun splitNotPrecise(numberOfParts: T) : List<RealNumberInterval<T>> {
        val diff = (end - start) / numberOfParts
        val realIntervals : MutableList<RealNumberInterval<T>> = ArrayList(numberOfParts.toInt() + 1);
        var incr = start
        do {
            realIntervals += RealNumberInterval(incr, incr + diff)
            incr += diff
        } while (incr < end)

        return listOf(RealNumberInterval(start, end))
    }
//others compare with math operations
}
What do you think about it?