Why smart cast doesn't work?

Hi!
I can’t understand why smart cast doesn’t work here, cause everything seems immutable and safe o_O

viewModelScope.launchSafe(
    body = {
        val firstPage = Page()
        val result: PageResult = getNotifications(type, firstPage)

        if (type == NotificationsType.CRITICAL && result.page.totalElements != null) {
            // Smart cast doesn't work there, it thinks that totalElements is still nullable o_O
            criticalCountNotifier.notify(result.page.totalElements)
        }
        .....

Where data classes and interface in question are:

data class PageResult(
    val page: Page,
    val list: List<Notification>
)
data class Page(
    ....
    val totalElements: Int? = null
)
interface CriticalNotificationsCountNotifier {
    fun notify(count: Int)
}

I don’t know internal details of smart casts, but I image it would be quite complicated to smart cast property chains (or maybe I’m wrong?). You should be able to do this with:

val totalElements = result.page.totalElements
if (type == NotificationsType.CRITICAL && totalElements != null) {
    criticalCountNotifier.notify(totalElements)
}

or:

if (type == NotificationsType.CRITICAL) {
    result.page.totalElements?.let { totalElements ->
        criticalCountNotifier.notify(totalElements)
    }
}

vals are read-only for you. They are not immutable! That means that totalElements may change (by this or another thread) between the test in the if expression and the use in the if block. The compiler cannot determine whether there is guaranteed to be no change, so does not allow use of totalElements.

You can use one of the solutions of @broot.

1 Like

I’m not sure if this is the case here. In the docs of smarts casts we can find that they are applicable for:

val properties - if the property is private or internal or the check is performed in the same module where the property is declared. Smart casts aren’t applicable to open properties or properties that have custom getters.

So it seems the compiler actually tries to distinguish read-only and truly immutable properties when smart casting. If it wouldn’t then smart casts of val properties would be at all disallowed.

In OPs example all properties are immutable and they match the requirements described above, so I see two possible explanations:

  • Smart casts do not work for chains,
  • OPs code is split into multiple modules, so compiler can’t guarantee immutability.
1 Like

It must be the second one, because this compiles (using Kotlin 1.5.20):

fun main() {
    val firstPage = Page()
    val result: PageResult = getNotifications(firstPage)

    if (result.page.totalElements != null) {
        criticalCountNotifier.notify(result.page.totalElements)
    }
}

private fun getNotifications(firstPage: Page) = PageResult(firstPage, emptyList())

class Notification

data class PageResult(
    val page: Page,
    val list: List<Notification>
)

data class Page(
    val totalElements: Int? = null
)

interface CriticalNotificationsCountNotifier {
    fun notify(count: Int)
}

object criticalCountNotifier : CriticalNotificationsCountNotifier {
    override fun notify(count: Int) {
        println("Notified")
    }
}
1 Like

Ahh, ok, so you’re probably right that the root cause is that the compiler can’t guarantee immutability.

@blinkev , if your data structures and launchSafe() code are placed in separate modules, then you can either move them to a single one (but it doesn’t make too much sense) or use one of examples I provided you.

2 Likes

@broot @jstuyts Thanks! The case was about different modules, you were right.
Btw the compiler says “Smart cast to ‘Int’ is impossible, because ‘result.page.totalElements’ is a complex expression” which is not the case obviously. Also it’s sadly that I can’t declare at module level that the field is a constant :frowning:

It looks simple. page and totalElements look like attributes, but are actually function calls. As your code is now, it is easy for a developer to see that it is safe to smart cast.

But because they are function calls, the module from which they originate can change the behavior completely while still being binary compatible. So the complexity the compile is talking about, should be interpreted as: the behavior can change in the future, and I am unable to determine/predict if the value returned by the first invocation will always be returned by further invocations.

1 Like

I tried to add @JvmField to the totalElements and the error message is the same. Shouldn’t it be changed to “smart cast is impossible because of separate modules”?

That is for interoperability with Java. It does not change the semantics of Kotlin.

That would make understanding the error a lot easier for a lot of people.

Interesting that today I ran into a situation where the compiler gave me a correct explanation in such a similar case “Smart cast to ‘OperationResultDto’ is impossible, because ‘response.result’ is a public API property declared in different module” :smiley: