Why have the reified keyword in Kotlin, isn't marking a function inline sufficient?
Solution 1:
Reified type parameters are requiring type arguments passed in them to be reified as well. Sometimes it's an impossible requirement (for instance, class parameters can't be reified), so making all parameters of inline functions reified by default would make it impossible to call ALL inline functions in cases when now it's only impossible to call ones with reified type parameters:
inline fun<T> genericFun(x: T) {}
inline fun<reified T> reifiedGenericFun(x: T) {}
class SimpleGenericClass<T>() {
fun f(x: T) {
genericFun<T>(x) //compiles fine
reifiedGenericFun<T>(x) //compilation error
}
}
UPDATE. Why not automatically infer "reifibility" based on the context?
- Approach 1 (suggested by @Tenfour04): Analyze code of inlined function and consider its type parameter as reified if it has
T::class
calls (I'd also addedis T
calls). - Approach 2 (suggested by @SillyQuestion): Consider all type parameters of inline functions as reified by default; if it leads to compilation error on usage site, then fallback to non-reified type.
Here is a counter-example to both: "a" as? T
. Function with this body would have different semantics depending on whether or not its type parameter is declared (or, hypothetically, inferred) as reified:
inline fun<reified T> castToReifiedGenericType() = "a" as? T
inline fun<T> castToSimpleGenericType() = "a" as? T
fun main() {
println(castToReifiedGenericType<Int>()) //null
println(castToSimpleGenericType<Int>()) //a
}
/*P.S. unsafe cast ("a" as T) also have different semantics for reified and non-reified T,
causing `ClassCastException` in the first case and still returning "a" in the latter.*/
So, with the first approach, semantics would change if we add a meaningless call to T::class
/is T
somewhere inside the inline function. With second - semantics would change if we call this function from the new site (where T
can't be reified
, while it was "reifiable" before), or, сonversely, remove a call from this site (allowing it to be reified
).
Debugging problems coming from these actions (at first glance unrelated to observing semantic changes) is way more complex and panic-inducing, than adding/reading an explicit reified
keyword.
Solution 2:
As @Михаил Нафталь's answer demonstrates, a type being reified
is very limiting, so it is critical that the language requires you to be explicit about exactly which types should be reified. Reification requires the type to be known at compile time, so functions with reified types can only be called from functions where that type is not a non-reified generic type.
Someone could argue, well then, only assume it is reified if T::class
happens to be used inside this inline function and otherwise treat it as not reified. But that would mean your effective function signature could change just by changing the contents of the function without changing its declaration. That would make it very easy to accidentally change a function signature, which is a recipe for disaster in the future. For example, suppose I have this function:
inline fun <T> registerList(list: List<T>) { // T is not reified
myProtectedRegistryList.add(list)
}
and so it is used in other places in my app, or by users of my library like this:
class Foo<T>(val data: List<T>) {
init {
registerList(data)
}
}
// or
fun foo(data: List<T>) {
registerList(data)
}
// or
class Bar<T> {
var barRegister: (List<T>)->Unit = ::registerList
}
Later I modify my function without changing its declaration:
// In hypothetical Kotlin with implicit reification, T is now reified:
inline fun <T> registerList(list: List<T>) { // This line of code unchanged!
myProtectedRegistryMap.put(T::class, list)
}
Now I have broken the code everywhere that it was used like one of the examples above. So, by the language requiring you to change the declaration to change the signature, you are forced to think about the external impact of modifying your function. You know that if you refactor the declaration of any function, that is the only way its usability is affected at call sites.
The Kotlin design philosophy on this kind of matter is to be conservative and require explicit declarations. It's the same reason functional interfaces have to be explicitly marked with the fun
keyword even if they currently only have a single abstract function, and classes/functions are final by default.