Graham Borland
Graham Borland

Reputation: 60721

Check reified generic type at compile time

I have a function which checks its generic type parameter, and if the type is one of the expected values, does something with it appropriate for that type. If the type is unexpected, it throws an exception.

inline fun <reified T> convert(functionName: String, vararg args: Any?): T {
    val obj: Any = executeJSFunction(functionName, *args)
    val builtInClasses = arrayOf<KClass<*>>(Int::class, String::class, Boolean::class)

    @Suppress("UNCHECKED_CAST")
    return when {
        T::class in builtInClasses -> 
            obj as T
        T::class.companionObjectInstance as? DataClassFactory<T> != null -> 
            (T::class.companionObjectInstance as DataClassFactory<T>).fromV8Object(obj as V8Object)
        else -> 
            throw IllegalArgumentException("No converter for type ${T::class}")
    }
}

This works, but it's doing the check at runtime. I'd like to find a way of getting a compilation error, instead of a runtime exception, if the generic type parameter is not one of the expected types. Is this possible, perhaps with contracts?

// should compile, as Int is one of the supported types
val intResult: Int = convert("intFunction")         

// should fail to compile, as Double is unsupported
val doubleResult: Double = convert("doubleFunction")

Upvotes: 4

Views: 643

Answers (1)

Sam
Sam

Reputation: 10016

There's no way to express those type constraints using the features currently available in the Kotlin compiler.

First, Kotlin doesn't (yet) have union types. Union types give you the ability to combine rules using a logical "or". If a value can be an Int, a String or a Boolean, its type is a union of those three. The Kotlin compiler doesn't have a way to express or even represent such a type.

As well as those three built-in types, the union type you want to express also contains classes whose companion object implements a particular interface. Although a class and a companion object are linked at compile time, there's no relationship between them in the type system. A type doesn't contain any information about whether it has a companion object, or what methods and interfaces that companion object might implement. You can't express the rule "T must be a type with a companion object that implements a given interface".

Because you can't denote or represent union types, you also can't write a contract that smart-casts the return value to a union type. There's no "or" syntax in contracts, and there would be no way to denote the type of the returned value. Besides, contracts don't themselves create or enforce rules about types. All they can do is provide information about the behaviour of a function, which can be used by the type system to feed into existing rules.

I think that reified generics might not actually be the right solution here. If you're using a reified generic type, that means that you already know the exact type of the expected return value. So instead of relying on the type system to deduce what you want to do, why not just tell it?

fun <T> convert(functionName: String, converter: Converter<T>, vararg args: Any?): T {
    val obj: Any = executeJSFunction(functionName, *args)
    return converter.convert(obj)
}

Now you can define the converters you need, perhaps as a sealed class/interface:

@Suppress("UNCHECKED_CAST")
sealed interface Converter<out T> {
    fun convert(obj: Any): T

    object IntConverter: Converter<Int> {
        override fun convert(obj: Any): Int = obj as Int
    }

    ...

    class DataClassConverter(val factory: DataClassFactory<T>): Converter<T>) {
        override fun convert(obj: Any): T {
            return factory.fromV8Object(obj as V8Object)
        }
    }
}

Now you can call your convert function like this:

val intResult = convert("intFunction", IntConverter)
val dataResult = convert("dataFunction", DataClassConverter(MyDataClass))

// doesn't compile because DoubleConverter doesn't exist
val doubleResult = convert("doubleFunction", DoubleConverter)

Upvotes: 3

Related Questions