How to implement floor modulo for every Number typ

2019-05-30 05:25发布

问题:

I'm currently learning Kotlin and trying to create an extension (infix) method that works on all number types (Byte, Long, Float, etc.). It should work like Python's % operator:

 4   %   3  ==   1      // only this is the same as Java's %
 4   %  -3  ==  -2
-4   %   3  ==   2
-4   %  -3  ==  -1

...or like Java's Math.floorMod, but it should also work with Double or Float:

-4.3 %  3.2 ==   2.1000000000000005

or with any possible combination of these types

 3   %  2.2 ==   0.7999999999999998
 3L  %  2.2f ==   0.7999999999999998

The following works as intended, but only for two Double or two Int:

inline infix fun Double.fmod(other: Double): Number {
    return ((this % other) + other) % other
}

inline infix fun Int.fmod(other: Int): Number {
    return ((this % other) + other) % other
}

// test
fun main(args: Array<String>) {
    println("""
            ${-4.3 fmod 3.2} == 2.1000000000000005

            ${4 fmod 3} == 1
            ${+4 fmod -3} == -2
            ${-4 fmod 3} == 2
            ${-4 fmod -3} == -1
    """)
}

Replacing Int with Number, I get the following error messages:

Error:(21, 18) Unresolved reference. None of the following candidates is applicable because of receiver type mismatch: 
@InlineOnly public operator inline fun BigDecimal.mod(other: BigDecimal): BigDecimal defined in kotlin
Error:(21, 27) Public-API inline function cannot access non-public-API 'internal open fun <ERROR FUNCTION>(): [ERROR : <ERROR FUNCTION RETURN TYPE>] defined in root package'
Error:(21, 36) Public-API inline function cannot access non-public-API 'internal open fun <ERROR FUNCTION>(): [ERROR : <ERROR FUNCTION RETURN TYPE>] defined in root package'

How can I achieve this for every number type without copy-pasting this for every type combination?

回答1:

The only reasonable option (also the fastest) is to define the operator for each pair of types you want to support:

infix fun Double.fmod(other: Double) = ((this % other) + other) % other

infix fun Int.fmod(other: Int) = ((this % other) + other) % other

infix fun Double.fmod(other: Int) = ((this % other) + other) % other

infix fun Int.fmod(other: Double) = ((this % other) + other) % other

That way the decision of what type to use is made by the compiler, not at the runtime. Those functions are not generic and do not use inheritance (read Number), which means that the values are not boxed (see Java primitive boxing), meaning that objects are not allocated.

I strongly do not recommend inlining those functions. Leave minor optimizations to JVM. The fact that objects are not allocated is the biggest performance win here.

P.S The number of functions grows as a square of types supported. Are you sure you need to support all types?



回答2:

After several minutes toying around, I came up with a dirty approach to do what you want:

import java.math.BigDecimal
import java.math.BigInteger

inline infix fun <reified T: Number> T.fmod(other: T): T {
  return when {
    this is BigDecimal || other is BigDecimal -> BigDecimal(other.toString()).let {
      (((BigDecimal(this.toString()) % it) + it) % it) as T
    }
    this is BigInteger || other is BigInteger -> BigInteger(other.toString()).let {
      (((BigInteger(this.toString()) % it) + it) % it) as T
    }
    this is Double || other is Double -> other.toDouble().let {
      (((this.toDouble() % it) + it) % it) as T
    }
    this is Float || other is Float -> other.toFloat().let {
      (((this.toFloat() % it) + it) % it) as T
    }
    this is Long || other is Long -> other.toLong().let {
      (((this.toLong() % it) + it) % it) as T
    }
    this is Int || other is Int -> other.toInt().let {
      (((this.toInt() % it) + it) % it) as T
    }
    this is Short || other is Short -> other.toShort().let {
      (((this.toShort() % it) + it) % it) as T
    }
    else -> throw AssertionError()
  }
}

assert(BigDecimal("2.1") == BigDecimal("-4.3") fmod BigDecimal("3.2"))
assert(BigInteger("2") == BigInteger("-4") fmod BigInteger("3"))
assert(2 == -4 fmod 3)
assert(2L == -4L fmod 3L)

assert(0.7999999999999998 == 3 fmod 2.2)
assert(0.79999995f == 3L fmod 2.2f)

I though reified would make casting (smart and explicit) unnecessary but it was not the case. Maybe I'm missing something (I'm new to Kotlin after all).



回答3:

Here's a completely generic higher-order-function approach without any reflection or casting:

inline fun <T> T.fmod(other: T, mod: T.(T) -> T, plus: T.(T) -> T) =
     this.mod(other).plus(other).mod(other)

assert(BigDecimal("2.1") == BigDecimal("-4.3").fmod(BigDecimal("3.2"), BigDecimal::mod, BigDecimal::plus))
assert(2L == -4L.fmod(3L, Long::mod, Long::plus))

However, it's not so pretty.



标签: kotlin modulo