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 nickname
s 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.
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:
Animal
(or whatever term fits better in your case, but Canidae and Felidae are families according to my 30 seconds of Googling :D).Animal
subclasses advertise their family as a property, so you can access the family of this
in your function.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 object
s 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}!")
}