How to write generic functions for all numeric types?


#1

I would write the following function able to work with all the numeric types (not only longs):

fun nonNegativeDifference(previous: Long, current: Long): Long =
    if (previous < current) current - previous
    else current

Is there a way to write it without to declare all the overloads?

I’ve tried to do the following but this kind of constraint is not supported:

fun <T> nonNegativeDifference(previous: T, current: T): T
    where T : Comparable<T>,
          T : interface { operator fun minus(other: T): T } =
    if (previous < current) current - previous
    else current

#2

There’s no completely satisfactory way to write generic functions for all numeric types.

However, one thing you could do which works in a fashion is the following:

operator fun Number.minus(other: Number): Number {
    return when (this) {
        is Long   -> this.toLong() - other.toLong()
        is Int    -> this.toInt()  - other.toInt()
        is Short  -> this.toShort() - other.toShort()
        is Byte   -> this.toByte() - other.toByte()
        is Double -> this.toDouble() - other.toDouble()
        is Float  -> this.toFloat() - other.toFloat()
        else      -> throw RuntimeException("Unknown numeric type")
    }
}

@Suppress("Unchecked_cast")
fun <T> nonNegativeDifference(previous: T, current: T): T 
    where T: Number, T: Comparable<T>  =
    if (previous < current) (current as Number - previous as Number) as T
    else current

fun main(args: Array<String>) {
    val p = 5L
    val c = 6L
    val r = nonNegativeDifference(p, c)
    println(r) // 1    

    val p2 = 5.toByte()
    val c2 = 7.toByte()
    val r2 = nonNegativeDifference(p2, c2)
    println(r2) // 2

    val p3 = 5.0
    val c3 = 8.0
    val r3 = nonNegativeDifference(p3, c3)
    println(r3) // 3.0 
} 

Of course, you can’t mix types here - the types of ‘previous’, ‘current’ and hence the result must be exactly the same.


#3

An analogy is the sum() extension method in Collection where all the overloads are defined. I personally prefer this approach over pattern matching, because method dispatch for the former is done at compile time and not at runtime.

In Scala the sum() method only exists once thanks to implicit conversions. The price that has to be paid for those implicits is well known (readability, compile times, unexpected results).

It would be interesting to see how the sum method is implemented in C# which has an implicit conversion operator. But in the same way as in C++, and very contrary to Scala, the implicit conversion operator in C# (which is operator()) is bound to the class from which instances are converted to instances of some other class (in Scala the implicit conversion operator can be defined in any class you like).


#4

C# also solves this with overloads: https://msdn.microsoft.com/en-us/library/system.linq.enumerable.sum(v=vs.110).aspx


#5

Not being able to write generic functions for the numeric types has been a thorn in the flesh of C# developers for many years as there isn’t even a common super-class or interface for these types (Kotlin does at least have the Number super-class).

I think if I had to write something like this myself I’d probably code two overloads: one for Long and one for Double. The overloads for the other numeric types are then just ‘one liners’ which delegate to the ones with the actual code.


#6

Not being able to write generic functions for the numeric types has been a thorn in the flesh of C# developers for many years as there isn’t even a common super-class or interface for these types

I see. Interesting. I always thought the C# designed things better than in the Java world. Well, then +1 for Kotlin ;-).

I believe it’s a general problem that is not easy to solve. Implicit conversions “in the wild” as in Scala has too drastic consequences that it could be a solution for something. This is just my opinion. I don’t want to spark a discussion on this now …

I once out of interest in Scala defined some new type that can be converted to numeric. I wanted to see whether my new type now also works with sum(). And it didn’t. Spent about 1/2 hour to find out what is missing also searching the Internet, but didn’t manage. Then I gave up, because if its that complicated it simply shouldn’t be that way.


#7

C# is not designed better it just broke backward compatibility few times to fix Java’s mistakes.

As for Java/kotlin, there is an effort currently ongoing to design better type system for numbers: https://github.com/apache/commons-numbers. One of the things one should consider is that to design truly universal number generics, one needs to also provide some kind of context for arithmetic operations. For example, BigDecimal already requires precision to work.


#8

(Kotlin does at least have the Number super-class)

This comment sparked my interest. So I checkedout Kotlin’s Number class. If I’m not mistaken Koltin’s Int class just implements java.lang.Number. If it implement a Number class created for Kotlin it coudl require all subclasses to implement + or plus(…) and all the other operators and that would be it. Only problem remaining would be how to detect the return type of the sum method. What if the collection contains floats, ints and longs? WHat is then the type of the sum returned?


#9

The problems with having the super-class implement the arithmetic operations (be it java.lang.Number or some other class specially invented for the purpose) are:

  1. Numbers would have to be boxed before they could be operated on because the super-class wouldn’t itself be a ‘value’ type.

  2. Although it would be possible to include a giant when statement in the operator functions to pattern-match against all possible combinations of the numeric types, the return value would inevitably have to be an object of the super-class itself as Kotlin doesn’t support union types. That means that you would have to ‘know’ what type to expect for each operation so that you could unbox the return value by casting it to the expected type.

So, in other words, it would neither be performant nor convenient and this is no doubt why each primitive type in Kotlin implements its own operations.

Of course, you can do things like I did in my opening post if you can live with these problems but it’s really only a technique that should be used for special purposes.