Kotlin Number class mystery

I really love kotlin, some of the language features and design solutions are magnificent, it is one of the nicest languages I’ve ever worked with. But there is one design feature in kotlin that makes me wonder.
Why doesn’t Number class (supertype of all primitive numbers in kotlin) override mathematical operators like plus, minus or multiplication? Every its ancestor overrides them - and it is very logical, those operations are for sure defined for all number types.
My case is: I’d like to implement a custom realization of vector, a pair of numbers, with some custom methods. I’d also like this vector to be a template class, so it would be possible to create a vector of integers or a vector of floating point numbers. But it is actually impossible (afaik) in kotlin just because in that case there is no guarantee those primitive mathematical operators would be available.
Example code:

class Vector <Type: Number> (var x: Type, var y: Type) {
    operator fun minus (other: Vector<Type>) = Vector(x - other.x, y - other.y)
    operator fun plus (other: Vector<Type>) = Vector(x + other.x, y + other.y)
}

That code won’t compile. But it seems to me that it would be more logical if it would!
Is there any general solution to that kind of problems?

1 Like

If Number defined, say, an operator +, that’d mean you could apply + to 2 Numbers of any kind, like an Int and a Double.
That operation is not defined in kotlin, you can only sum 2 Numbers of the same type.

If there were self types in the language probably you could define operations on Number.

1 Like

It would be possible if Number class itself supported generic plus() method. This is an example how it could work:

interface UnaryPlusOperator<T: Any> {
	operator fun plus(other: T): T
}

abstract class Number<SELF: Number<SELF>> : UnaryPlusOperator<SELF> {
	// rest of the body
}

class Int : UnaryPlusOperator<Int> {
	override fun plus(other: Int) = this + other
	
	// rest of the body
}

Well, most likely it won’t be possible though, because Java Number class wouldn’t support this.

An another solution would be by using a duck typing, but I wouldn’t expect Kotlin team to implement it.

Number shouldn’t necessively define + operator for two Numbers. It could have defined + for all its ancestors, as it is already done in, for example, Float.
I made this example on playground: Playground Example
Other operators could be considered in the same way.

There is no way to define operations on generic numbers that would suit everyone. For example consider those problems:

  1. Number is not sealed. What happens if someone defines a new number? What is the type of the resulting operation?
  2. The plus operation is in general considered to be symmetric. In your case, the result will be different depending on the order.

The number problem is not fully solved in any programming language. Right now I am preparing a talk on Joker conference about that. They asked to do it in Russian, but I can repeat it later in English somewhere.

So the short answer is that if you want operations on numbers, you do a local extension function that works only for your code. Anothe way is to go fully generic and use operations scopes like we do in KMath.

As always, all discussions are welcome in kotlin slack #mathematics channel.

5 Likes

What’s the problem of this definition fun<T: Num> plus(a: T, b: T): T?

That is, plus(Int, Int) or plus(Float,Foat) typechecks, but plus(Int,Float) does not.

1 Like

You should verify your claim before posting :stuck_out_tongue_winking_eye: Yes, you can provide Int and Float to such a function. T would be Number in that case.

2 Likes

Well, if it was in standard library then it would not be the problem, because annotations from kotlin.internal could be used to enforce specific behavior of type inference, e.g.

package kotlin.internal

@Target(AnnotationTarget.TYPE)
@Retention(AnnotationRetention.BINARY)
internal annotation class Exact

@Target(AnnotationTarget.TYPE)
@Retention(AnnotationRetention.BINARY)
internal annotation class NoInfer

// Hack above - you can copy Kotlin's internal annotations to your module
// to use them in your module. Not recommended for production ;)

fun <T: Number> plus(a: @Exact T, b: @Exact T): Int {

    return a.toInt() + b.toInt()
}

fun <T: Number> @Exact T.plus2(b: @Exact T): Int {

    return this.toInt() + b.toInt()
}

fun <T: Number> plus3(a: T, b: @NoInfer T): Int {

    return a.toInt() + b.toInt()
}

fun <T: Number> T.plus4(b: @NoInfer T): Int {

    return this.toInt() + b.toInt()
}

fun main() {
    val int: Int = 5
    val double: Double = 3.7

    plus(int, int)
    plus(double, double)
    plus(int, double) // errors for both arguments - because parameters must have equal types
    
    int.plus2(int)
    double.plus2(double)
    int.plus2(double) // errors for both arguments - because parameters must have equal types

    plus3(int, int)
    plus3(double, double)
    plus3(int, double) // error for second argument only - because inference takes into account only first parameter
    
    int.plus4(int)
    double.plus4(double)
    int.plus4(double) // error for second argument only - because inference takes into account only first parameter
}

However, as these annotations are internal, I believe they cannot really be used on an operator fun in Number, because then it cannot be subclassed outside of the standard library.

As a bit on a side note, it would be nice for these annotations to be made public or promoted to some language construct, because they could be really helpful for designing APIs using generics. I guess it could be especially useful for library writers.

3 Likes

These still wouldn’t work if you had your int and double defined as being of type Number:

package kotlin.internal

@Target(AnnotationTarget.TYPE)
@Retention(AnnotationRetention.BINARY)
internal annotation class Exact

@Target(AnnotationTarget.TYPE)
@Retention(AnnotationRetention.BINARY)
internal annotation class NoInfer

// Hack above - you can copy Kotlin's internal annotations to your module
// to use them in your module. Not recommended for production ;)

fun <T: Number> plus(a: @Exact T, b: @Exact T): Int {

    return a.toInt() + b.toInt()
}

fun <T: Number> @Exact T.plus2(b: @Exact T): Int {

    return this.toInt() + b.toInt()
}

fun <T: Number> plus3(a: T, b: @NoInfer T): Int {

    return a.toInt() + b.toInt()
}

fun <T: Number> T.plus4(b: @NoInfer T): Int {

    return this.toInt() + b.toInt()
}

fun main() {
    val int: Number = 5
    val double: Number = 3.7

    plus(int, int)
    plus(double, double)
    plus(int, double) 
    
    int.plus2(int)
    double.plus2(double)
    int.plus2(double) 
    plus3(int, int)
    plus3(double, double)
    plus3(int, double)     
    int.plus4(int)
    double.plus4(double)
    int.plus4(double) 
}
1 Like

After all I personally came with this solution:

inline operator fun <reified T: Number> T.plus (other: T) = when (T::class) {
    Byte::class -> toByte() + other.toByte()
    Short::class -> toShort() + other.toShort()
    Int::class -> toInt() + other.toInt()
    Long::class -> toLong() + other.toLong()
    Float::class -> toFloat() + other.toFloat()
    Double::class -> toDouble() + other.toDouble()
    else -> throw ClassCastException("Type ${T::class.simpleName} can not be casted to any known Number type!")
} as T



data class Vector <Type: Number> (var x: Type, var y: Type)

inline operator fun <reified Type: Number> Vector<Type>.plus (other: Vector<Type>): Vector<Type> = apply { x += other.x; y += other.y }



fun main () {
    var intVector = Vector(3, 5)
    intVector += Vector(13, 11)
    println(intVector)

    var doubleVector = Vector(3.6, 1.4)
    doubleVector += Vector(33.0, 35.2)
    println(doubleVector)
}

It’s neither beautiful nor handy: there is an example of resulting Vector class - due to reified nature of plus function class methods using that function should be extensions (akaik we don’t have reified params for template classes (yet?)).
Moreover, now I think that it would be easier to use Doubles everywhere and cast them to desired classes where needed. Maybe using other superb kotlin language design features like inline properties…