Search code examples
kotlingenericsenumsvariadic-functions

function vararg parameter for Enum<T> in kotlin


I have the following Enums where each Enum class implements RatingValue interface and each companion object implements RatingValues<T> interface

enum class Clarity(override val value: Int) : RatingValue {
        EXCELENT(5),
        VERY_GOOD(4),
        GOOD(3),
        FAIR(2),
        POOR(1);

        companion object : RatingValues<Clarity>
    }

    enum class Colour(override val value: Int) : RatingValue {
        EXCELENT(10),
        VERY_GOOD(8),
        GOOD(6),
        FAIR(4),
        POOR(2);

        companion object : RatingValues<Colour>
    }

RatingValues interface has ratings() method, which is defined as an extention:

inline fun <reified T> RatingValues<T>.ratings(): List<Int> where T : RatingValue, T : Enum<T> = enumValues<T>().map { it.value }

I'd like to implement a method that takes a vararg parameter of RatingValue enums which would be callable like this

val cumulativeRating = cumulate(Colour, Clarity)

My first idea was to write the following, however that fails since the generic type parameter T for RatingValues is obviously different

private inline fun  <reified T> cumulate(vararg ratings: RatingValues<T>) : List<Int> where T: RatingValue, T : Enum<T>  {
        return ratings
            .map(RatingValues<T>::ratings)
            .fold(listOf(0, 0, 0, 0, 0)) { x, y -> x.zip(y, Int::plus) }
}

Solution

  • Method which could accept vararg RatingValues<T> with different T is:

    private fun cumulate(vararg ratings: RatingValues<*>): List<Int> {
        return ratings
            .map { it.ratings() }
            .reduce { x, y -> x.zip(y, Int::plus) } //same semantics, but more concise and performant
    }
    

    The problem is that due to type erasure, information about actual type of T would be lost, so it will be reified as Object and you'll get rather cryptic runtime error: java.lang.NoSuchMethodError: java.lang.Object.values()[Ljava/lang/Object; (in my opinion, compiler shouldn't have compiled this in the first place, but that's not the point).

    I'm afraid, you' ll have to define ratings() method in the RatingValues interface as non-generic and implement it in every enum's companion object to make it work correctly:

    interface RatingValues<T> where T : RatingValue, T : Enum<T> {
        fun ratings(): List<Int>
    }
    
    inline fun <reified T> RatingValues<T>.ratingsForEnums(): List<Int> where T : RatingValue, T : Enum<T> =
        enumValues<T>().map { it.value }
    
    enum class Clarity(override val value: Int) : RatingValue {
        EXCELENT(5),
        VERY_GOOD(4),
        GOOD(3),
        FAIR(2),
        POOR(1);
    
        companion object : RatingValues<Clarity> {
            override fun ratings() = ratingsForEnums()
        }
    }
    
    enum class Colour(override val value: Int) : RatingValue {
        EXCELENT(10),
        VERY_GOOD(8),
        GOOD(6),
        FAIR(4),
        POOR(2);
    
        companion object : RatingValues<Colour> {
            override fun ratings() = ratingsForEnums()
        }
    }