7

In Kotlin, given that the 'reified' keyword can only be used for generic type parameters of inline functions, why have the reified keyword at all? Why can't the Kotlin compiler (at least in the future) automatically consider all generic type parameters of inline functions as reified?

I have seen that people panic when looking at this 'reified' word and ask me not to make the code complex. Hence the question.

  • 2
    Does this answer your question? [How does the reified keyword in Kotlin work?](https://stackoverflow.com/questions/45949584/how-does-the-reified-keyword-in-kotlin-work) – Karsten Gabriel Jan 19 '22 at 09:25
  • 2
    I'm kind of amazed it is possible for someone to get hired in a programming job if the concept of a reified type is panic-inducingly complex. – Tenfour04 Jan 19 '22 at 16:05
  • 6
    **This is not a duplicate of that other question!** It's asking about the design reasoning for why the `reified` keyword cannot be implicit. The OP obviously understands what the keyword does and means. – Tenfour04 Jan 19 '22 at 16:18

3 Answers3

4

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?

  1. 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 added is T calls).
  2. 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.

  • The question implied only those parameters that allow the 'reify' keyword to be used on them. Wouldn't the compiler be able to know when a parameter can't be reified and not reify it in that case? That could still achieve the goal of eliminating this 'reified' keyword that does not seem to have much practical significance. – SillyQuestion Jan 19 '22 at 14:29
  • What do you mean by "not reify"? If some inline function uses `T` as a reified type (it's not only `T::class` calls, but also `is T` checks and `as T` casts), what it should do with "not reified" parameter? Use `Any?` instead? Obviously, it just doesn't make sense. – Михаил Нафталь Jan 19 '22 at 21:17
  • By 'not reified' I mean that: Imagine that the compiler works as follows like a code assist tool: For every inline function's type parameters, it checks whether if it automatically adds 'reified', does that result in a compilation error or not. If it results in a compilation error, then do not add 'reified' keyword. – SillyQuestion Jan 20 '22 at 10:45
  • What if it results in a compilation error when automatically 'reified' keyword was added (in some usage site of inline function), so it doesn't add 'reified' keyword, but it also results in compilation error (inside the inline function itself). How compiler should report this error? Without knowing what was the intention of a developer (explicitly expressed with `reified` keyword/with its absence), the compiler is unable to correctly point to the line causing the error. – Михаил Нафталь Jan 20 '22 at 15:33
  • Another example: it compiles fine both with and without the `reified` keyword, but throws runtime exception only with the `reified` keyword. – Михаил Нафталь Jan 20 '22 at 15:34
  • In scenario 1, any compile-time exception regardless of the site of error should cause the automatic reified not to be added. If even without reified, there is still a compile error, then I think that error can be reported. In the second example, the reified keyword will automatically get added and exception will be thrown which is ok. – SillyQuestion Jan 20 '22 at 18:22
  • I don't think getting a runtime exception is ok, especially if it wasn't thrown before a new call to the same inline functions was added to the codebase/or some old call removed. It's basically a very unintuitive semantics change. See my answer's update. – Михаил Нафталь Jan 20 '22 at 21:16
  • 1
    Another "elephant in the room" is Java interop - inline functions with `reified` keyword can't be called from Java (they are compiled as [`synthetic`](https://blog.frankel.ch/synthetic/)) – Михаил Нафталь Jan 20 '22 at 21:55
4

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.

Tenfour04
  • 83,111
  • 11
  • 94
  • 154
  • Edited since I originally answered without noticing the other answer. I've removed my redundant and more complicated explanation of the same thing. – Tenfour04 Jan 19 '22 at 16:01
  • Can you please provide an example to visualize how the effective function signature can change, by changing the content of the function rather than its declaration? – SillyQuestion Jan 19 '22 at 17:57
  • I added an example. – Tenfour04 Jan 19 '22 at 18:36
  • By 'code is broken', do you mean a compile time error? Is it that the interface of the binary is changing, and so the client binaries will not work with the new binary (jar file) in which we define registerList ? But in that case, why can't reified keyword be implied always for all types for which reified keyword can be applied? Then, the issue of function signature change would not arise. – SillyQuestion Jan 20 '22 at 10:40
  • Yes, it would create compile errors. If reified were always enforced, then you could never use an inline generic function in the three situations I showed above. Reified types are more limited as my answer explains. – Tenfour04 Jan 20 '22 at 13:16
  • Can't the same function be inlined differently based on different call sites, with different implicit reification, and different errors be reported based on different call sites? – SillyQuestion Jan 20 '22 at 18:24
  • It doesn't matter what the errors are. The point is to avoid compile errors in the first place. Once a function changes to start requiring reification, it can no longer be used as in the three examples above. Calling code would go from working to uncompilable. And in the third example, we have code where it is not inlined at all, so then you're also dealing with binary compatibility issues in libraries. Reification makes a function inline-only, effectively missing from the class definition, so adding reification would break binary compatibility. – Tenfour04 Jan 20 '22 at 18:31
  • If you're not familiar with binary compatibility, here's an example. You make library A with class Foo, version 1. Library B made by someone else uses library A version 1 as a dependency. Then you create version 2 of library A with a few changes to class Foo. A user creates a project using Library B and version 2 of Library A. As long as your changes to class Foo are binary compatible, the compiled version of Library B in the user's project will still work even with the newer version of Library A. Reification changes aren't binary compatible. – Tenfour04 Jan 20 '22 at 18:37
0

Accepting the answer from @Михаил Нафталь (the first one provided and further updated) with a warm thank you to @Tenfour04 as well. Just thought to add a (hopefully) more simplified answer based on my understanding of the answers provided, that would still be essentially correct:

  • A practical (but may not be complete) definition of a 'reifiable type' is that if the type is something like List<T>, we can evaluate T::class.java from it. Such a reifiable type has probably not had type erasure done on it, that is something that the java compiler does on type parameters to reduce the size of the compiled binary. It looks like at present, there are certain cases where the type erasure is being done by legacy java compiler etc. and kotlin does not yet provide a way to customize that (i.e. prevent it in certain cases).
  • Unfortunately, from just the expression List<T> it cannot be made out whether the type is reifiable, or not. It is as if there could have been a keyword added for the developer to see this explicitly, for example: List<nonerased T> and List<erased T>. But at present, the compiler detects this based on where/how T was defined, otherwise the language might become too verbose.
  • In non-inline functions, all generic type parameters become non-reifiable (erased), probably due to jvm type erasure at compile time, in line with the usual (or currently technically not yet addressed) behavior of the java compiler.
  • However in inline functions e.g. inline fun <T> f1(list: List<T>){...}, there is an option to declare a type as reified T (which makes it inline fun <reified T> f1(list: List<T>){...}), which means it will accept only List<nonerased T> kind of parameters (that have this invisible 'nonerased' keyword associated with them), else it will give a compilation error. It will then not do type erasure for T. Due to this compile-time check, the user can now proceed to use T::class.java inside the inline function, with the assurance that they will not get an error at runtime because T was erased T and so T::class.java cannot be found.
  • The inline function without the visible 'reified' keyword will behave like a non-inline function and do type erasure as usual.