Search code examples
kotlingenericsinterface

Apparently mandatory "reified" keyword introduces weird side effects


I wan't to create a generic class around 2D arrays and enrich it with a bunch of helper functions. But reify keyword is giving me a lot of trouble. Here is a simplified example of what i have and the errors triggered by IJ (please ignore poor construction of object and possible IndexOutOfBoundException) :

class MyMatrix<T>(var matrix: Array<Array<T>>) {

    var height = matrix.size
    var width = matrix[0].size

    fun get(x: Int, y: Int): T = matrix[y][x]

    fun set(x: Int, y: Int, data: T) {
        matrix[y][x] = data
    }

    fun dump(transformer: (T?) -> String) {
        println("==== ($width x $height)")
        for (y in 0..<height) {
            print("#${y.toString().padStart(3, '0')} : ")
            for (x in 0..<width) {
                val t = get(x, y)
                print(transformer(t))
            }
            println()
        }
        println()
    }

    fun row(y: Int): Array<T> {
        return matrix[y]
    }

    inline fun <reified U> column(x: Int): Array<U> {
        return Array(height) { y -> matrix[y][x] as U }
    }
}


val aa = Array(3) { y ->
    Array<Int?>(6) { x -> y * 100 + x }
}
val m = MyMatrix(aa)

m.set(1, 1, null)

m.dump { i -> i?.let { "%03d ".format(i) } ?: "--- " }
// ==== (6 x 3)
// #000 : 000 001 002 003 004 005 
// #001 : 100 --- 102 103 104 105 
// #002 : 200 201 202 203 204 205 

// All getters return 204
println(m.get(4, 2))
println(m.row(2)[4])
println(m.column<Int>(4)[2])

Before getting to that result, I followed this path of trial and error for the column method :

Step A:

fun column(x: Int): Array<U> {
   return Array(height) { y -> matrix[y][x] }
}

producing the error on "return Array" : "Cannot use 'T' as reified type parameter. Use a class instead."

Step B:

inline fun <reified T> column(x: Int): Array<T> {
   return Array<T>(height) { y -> matrix[y][x] }
}

With, error on "matrix[y][x]" : "Type mismatch. Required: T#1 (type parameter of ...MyMatrix.column) Found: T#2 (type parameter of ...MyMatrix)"

Step C: Naming the 2nd T "U": "Type mismatch. Required: U Found: T"

inline fun <reified U>  column(x: Int): Array<U> {
    return Array(height) { y -> matrix[y][x] }
}

Step D: adding "as U" as shown in the above snipped. That seems to work provided i have to change the call site: val col = grid.column<Boolean>(x)

I am really not please with the workaround given that

  • implementation and signature is different between methods.
  • caller has to adapt
  • inlining produce side effects like changing visibility of other attributes/methods of the class / parent class.

I hope i did something wrong given my partial knowledge of generics in Kotlin. What would be the clean way to write a good generic library ? Thanks for your help.


Solution

  • row and column can be written as extension methods instead.

    inline fun <reified T> MyMatrix<T>.row(y: Int) =
        Array(width) { x -> get(x, y) }
    
    inline fun <reified T> MyMatrix<T>.column(x: Int) =
        Array(height) { y -> get(x, y) }
    

    This way,

    • their signatures are consistent
    • callers don't need to write the type parameter explicitly
    • what they do is also consistent - they both create a new array

    Note that I'm using get - This makes it possible to make matrix private.

    Also consider making the get method an operator:

    operator fun get(x: Int, y: Int): T = matrix[y][x]
    

    This allows you to use the [x, y] syntax on MyMatrixs.