Search code examples
kotlingenericspolymorphism

Is there any way to achieve static polymorphism in Kotlin?


I've run into a situation, where I got some classes that share the same interface:

sealed interface Animal {
    // "const static val nickname: String" ??
}

class Canidae: Animal {
    companion object {
        const val nickname = "dog"
    }
}

class Felidae: Animal {
    companion object {
        const val nickname = "cat"
    }
}

// and more ...

and I wanted to make a function that works like this:

inline fun<reified T: Animal, R> Animal.doSomething(f: (T) -> R): R =
    if (this is T) {
        f(this)
    } else {
        error("it's not a ${T.nickname}, it's a ${this.nickname}!") // but this simply can't compile
    }

I can assure that all classes which implement Animal have a compile-time constant nickname. Does there exist an approach of doSomething in Kotlin?

I've tried to just use reflections and enumerate over each derived class of Animal, and it surely worked. But that overcosts runtime resources, and it's tiring to maintain all nicknames inside a single function instead of where the class it associates with is defined. It would be very helpful if there's a more elegant way to do it.


Solution

  • Kotlin doesn't have the concept of static, but it has companion objects. Since companion objects are objects, you can make them implement interfaces.

    You could leverage this in the following (admittedly over-engineered) approach:

    • use a different type to represent the family of Animal (or whatever term fits better in your case, but Canidae and Felidae are families according to my 30 seconds of Googling :D).
    • make Animal subclasses advertise their family as a property, so you can access the family of this in your function.
    • make the companion object of each animal class implement the Family interface, so you can use the class name in code to refer to the family of any given Animal subclass (you could instead create regular objects for each family, but using companions allows to refer to the family using the class name)

    This gives the following:

    sealed interface Animal {
        val family: Family<*>
    }
    
    interface Family<T : Animal> {
        val nickname: String
    }
    
    class Canidae: Animal {
        
        override val family get() = Canidae
        
        companion object : Family<Canidae> {
            override val nickname = "dog"
        }
    }
    
    class Felidae: Animal {
        
        override val family get() = Felidae
        
        companion object : Family<Felidae> {
            override val nickname = "cat"
        }
    }
    
    inline fun <reified T : Animal, R> Animal.doSomething(family: Family<T>, f: (T) -> R): R =
        if (this is T) {
            f(this)
        } else {
            error("it's not a ${family.nickname}, it's a ${this.family.nickname}!")
        }
    
    

    And you can use it this way:

    animal.doSomething(Felidae) { println(it) }
    

    This implementation has a drawback, though. You could by mistake make the Canidae class expose the Felidae family without compilation error.

    We could fix this with even more over-engineering by making the Animal class recursively generic:

    sealed interface Animal<T : Animal<T>> {
        val family: Family<T>
    }
    
    interface Family<T : Animal<T>> {
        val nickname: String
    }
    
    class Canidae: Animal<Canidae> {
        
        override val family get() = Canidae
        
        companion object : Family<Canidae> {
            override val nickname = "dog"
        }
    }
    
    class Felidae: Animal<Felidae> {
        
        override val family get() = Felidae
        
        companion object : Family<Felidae> {
            override val nickname = "cat"
        }
    }
    
    inline fun <reified T : Animal<T>, R> Animal<*>.doSomething(family: Family<T>, f: (T) -> R): R =
        if (this is T) {
            f(this)
        } else {
            error("it's not a ${family.nickname}, it's a ${this.family.nickname}!")
        }