Feature request: static interface for type bounds

I have a generic function for querying an API that looks something like:

    inline fun <reified T : Any> queryApiX(
        queryName: String,
        accountToken: String,
    ): List<T> {
        val jsonNode = makeRequest(queryName, accountToken)
        return deserialize<T>(jsonNode)
    }

What I’d like is to be able to define the bound T : XQueryableEntity, such that all implementations of XQueryableEntity must have a static/companion object member “queryName”, so none of my call sites have to know about and repeat the query name and it can just be defined on the type:

    class XUser : XQueryableEntity {
        companion object {
            val queryName = "getUsers" // compile-time checked that this exists
        }
    }
    inline fun <reified T : XQueryableEntity> queryApiX(
        accountToken: String,
    ): List<T> {
        // accessible via bounded type parameter
        val jsonNode = makeRequest(T::queryName, accountToken)
        return deserialize<T>(jsonNode)
    }
    val users: List<XUser> = queryApiX(accountToken)

But there doesn’t seem to be any way to inherit any kind of static property that can be accessed in a type-safe way on a type parameter without having an instance of it.

2 Likes

Companion objects are completely independent from their type, so it isn’t possible to express this kind of requirement for a type.

However, you can do something like:

interface XQueryableEntity {
    …
    val metadata: XQueryableEntityMetadata
}

interface XQueryableEntityMetadata {
    val queryName: String
}

class XUser : XQueryableEntity {
    override val metadata get() = Companion

    companion object : XQueryableEntityMetadata {
        override val queryName = "getUsers"
    }
}

Then, from a given XUser, you can get its query name with user.metadata.queryName.

This pattern is used in a few places, most notably in KotlinX.Coroutines’ CoroutineContext.Key. In my own code, you can see it here.

3 Likes

This doesn’t help, the point is I need the queryName before I have any given XUser. I should clarify I’ve pretty much gathered you can’t express this kind of requirement, this is essentially a feature request.

One option would be to have your users pass in the implementation explicitly, but it can still hold the type, so that it does double duty as a type inference hint for T:

interface XQueryableEntityCompanion<T: Any> {
  val queryName: String
}
inline fun <reified T : Any> queryApiX(
        companion: XQueryableEntityCompanion<T>,
        accountToken: String,
) = ...

// You can even theoretically have this unsafe method if you want:
inline fun <reified T : Any> queryApiX(
        accountToken: String,
) = queryApiX<T>(T::class.companionObjectInstance, accountToken) // requires kotlin.reflect.full

//Usage
class XUser {
    companion object: XQueryableEntityCompanion<XUser> {
        val queryName = "getUsers"
    }
}
queryApiX(XUser, "foo)

I like this, good workaround.

I think you could make this safer by checking if the companion object is an instance of XQueryableEntityCompanion. I think you could also check if the type parameter of the XQueryableEntityCompanion instance is the same as the reified type that’s been provided.

yes, but either way it’s a runtime error. Compile-time safety is really the goal here

Yeah I know, it’s not possible to do it at compile time without extra steps. This is simply an improvement to @kyay10’s solution, since it’s currently not safe at all; it could fail at runtime with a pretty unhelpful error about a wrong class time or wrong parameter type. In fact, I’m not even sure his code would compile, because the compiler can’t guarantee that T::class.companionObjectInstance is an instance of XQueryableEntityCompanion.

I second this motion.

While I do not expect the team to implement something like C# static abstract, I could imagine a companion-generic-contraint, that would allow us to obtain the companion of a type safely.

1 Like

Then you can use the opposite solution;

interface XQueryableEntity { … }

interface XQueryableEntityMetadata<E : XQueryableEntity> {
    fun makeRequest(accountToken: String): List<E>
}

class XUser : XQueryableEntity {
  
    companion object : XQueryableEntityMetadata<XUser> {
        override fun makeRequest(accountToken: String) =
            makeRequest("getUsers", accountToken)
    }
}

inline fun <reified T : XQueryableEntity> queryApiX(
    metadata: XQueryableEntityMetadata<T>,
    accountToken: String,
): List<T> {
    val jsonNode = metadata.makeRequest(accountToken)
    return deserialize<T>(jsonNode)
}

val users = queryApiX(XUser, accountToken)

It’s basically the same idea as @kyay10 but slightly safer through the type bounds in the companion interface, and because the companion becomes responsible for the request.

1 Like