What is the reason behind Smart Cast being impossible to perform when referenced class is in another module?

@yole Ref discussion with @atomcat1978, perhaps one solution would be to generate the extra type information at compile-time, and embed it in the binary (f.ex. in the META-INF folder of the JAR).

This idea was inspired by how TypeScript libraries often ship type declarations in addition to executable JS.

That being said, I have little insight into the inner workings of the Kotlin compiler / JVM, so it’s hard for me to say if it’s workable, or even a good idea.

I’m not sure how you expect that generating extra information would help. It’s already possible to detect that a property is implemented as a direct access to a final field. The problem is that you can change the implementation, and this would invalidate the assumptions made when compiling the dependent module. Or do you suggest that the compiled code should access the metadata at runtime and refuse to function if the metadata differs from what existed when the module was compiled?

4 Likes

Actually, the new when syntax is going to help a lot in this sense:

// another module:::
data class ClassInOtherModule(val name: String?)

// module using the code:::
val instance = ClassInOtherModule(null)

when (val name = instance.name) {
    null -> println("")
    else -> println(name)
}

Your example is equivalent to much shorter:

println(instance.name ?: "")

3 Likes

It was an oversimplified example. Read the thread, and than you’ll see what I meant. Certainly I would not just write out the name property, but do some more stuff in a real life example.

when(val name = instance.name) {
   null -> doSomedefault()
   else -> callABuisnessMethod(name)
}

Previously it was not possible, you had to use the evil !! operator like this:

when(instance.name) {
   null -> doSomedefault()
   else -> callABuisnessMethod(instance.name!!) // since name is a public API property
}

You can solve this one with let as well, however, I think, the when version reads easier.

instance.name?.let {
   callABuisnessMethod(it)
} ?: doSomeDefault()
4 Likes

This is caused by the poor language design decision to add val get(). Without val get() kotlin could have more strongly declared that val means immutable, now it only means “read-only”. val get()'s are not worth the trouble they cause. In my team we have a convention to always declare functions for non-immutable data. It is a bug in the language in my opinion. I would support a breaking change to get rid of val get()s.

3 Likes

How exactly do you propose to change val to work? It has to be compatible with classes written in java. There is no way of ensuring immutability when working with third party libraries. You would need to drop java interop at least to make this change work.

Also it’s quite a common practise to have a public getter with private/protected setters. Maybe you don’t do that in your team, but I don’t see why kotlins property syntax should not be able to support this and I would most certainly not consider this a bug in the language.

If you propose to add a new keyword for immutable fields, I would like that but maybe that should be a different topic here (as it has nothing to do with the original question).

1 Like

I would propose to get rid of all the magic around property access and getter functions. A getter function would have no magic, just like java. Property access would just be naked access to the field. Same with property delegates, I don’t understand what we gain by complicating properties instead of just using functions.

1 Like

As you write yourself, you can already decide to declare functions to access private properties. By removing the properties feature from the language, we can’t gain immutability as you claim it.

1 Like

If val would mean the same as (public) final <type> in Java, the reference would be immutable and smartcasts could work. I’m with @miguelvargas here. Of course, implementing fields using getters is good style and for that, smartcasts wouldn’t work.

That’s not the reason why it does not work. In fact, smart casts on val properties DO work. But only if the accessed property is in the same module as the accessing code.

1 Like

That is exactly the reason why they don’t work. And in the local module the smartcasts only work because there Kotlin can be sure that you’re not implementing them using get(). In other modules, kotlin can’t be sure about that because replacing a simple val with a custom getter isn’t a breaking change.

More importantly, changes in classes can be made even at runtime (using classloaders). Even the assumption that within the module the class is stable somewhat exposes the system to ABI change related bugs.

Or Hibernate-like bytecode manipulation.

Thanks. I use the first when version on my JPA repository method.

It is more readable to check null.

@yole do you think it would be possible to consider adding some kind of configuration/compile time option to explicitly declare that you own certain modules in multi-module project and you are responsible not to break such contract and compile all projects together (e.g. gradle/maven) ?

It might make sense to have such option and to tell the compiler “I know better and it’s me to blame if it’s not the case”.

From my point of view it’s aligned with “being pragmatic” approach and similar to existing features like !! or late init var, etc.
Does it make sense to you?

5 Likes

It was already mentioned here a few times, last time in @AlexTrotsenko comment. Cannot we instruct compiler, that we “trust” certain modules and take the responsibility if things don’t work at runtime? It is pretty annoying, when in my multi-project build I have to choose between benefiting from smart casts and breaking down codebase into separate projects for clean reuse. In my case, I know the code is always compiled together and put into Docker container where it won’t change and yet compiler won’t let me benefit from smart cast because of theoretical possibility, which does not happen in practice.

I see though, how this is a real problem when using external libraries or even when using library from the same multi-project build in another library, as we cannot guarantee that both modules will resolve to the same version in the project they are used.

I’m curious what’s your opinion on it @yole. How do you see such compiler option for some projects?

3 Likes

For the record I’m no longer involved in the design of Kotlin. My personal opinion is that adding command-line switches for drilling holes in the type system is a bad idea and should not be done; however, I’m not the one who you need to convince if you want to see some changes here.

4 Likes

See also corresponding YouTrack issue.

1 Like

It would be amazing to instruct the compiler to “trust” modules that we own (consider Gradle multi-project build).

Right now this is forcing us to put everything in a single module.