Smartcast for map containsKey

Map definition is <Long, Product> (not nullable product)
if(productsMap.containsKey(productid)) {
val product = productsMap[productid]!!
}

In the above case i need to add the !! to not get a nullable val.
It would be awesome if there could be a smartcast like the nullchecks that understands that if the key exists it cannot be null.

I searched for a similar post but could not find any. (mybe i am not a good searcher as i imagine people have encountered this much more often than i do) So in advance appologies if this a repeat of a different topic.

I think it is not wanted because in a multi-threaded context, map could be modified between the two operations (containsKey and get). But I’m not 100% sure of my explanation, I’d like other people advice if possible.

If your aim is to retrieve a value anyway, I’d rather directly use get operator, as it avoids performing search twice :

productMap[productId]?.let { product -> 
   // Do stuff with it here
}
4 Likes

Your code is a lot more elegant than mine in any case :slight_smile:

This works the same as any passed variable where you perform a nullcheck and does get smartcasted I think.
If the map is changed i would expect a ConcurrentModificationException. Even so you could make the cast for the non mutable maps maybe?

1 Like

ConcurrentModificationException is not guaranteed to be thrown. The documentation says this:

This exception may be thrown by methods that have detected concurrent modification of an object when such modification is not permissible.

Note the word “may”. It’s only thrown if the container is able to detect it.

1 Like

Capturing the value is the only way to keep null-safety (currently).

Although we can guess that the map implementation we’re using probably won’t return null after returning some non-null value we can never be sure (and the compiler even more so).

Since map[key] is just another method call, I think what you really want is some kind of promise from the author that when a method returns notNull, it won’t return null in subsequent calls:

fun someMethod(): String? {
    contract {
        // Some contract statement.... I'm not familiar enough to guess what it would look like.
    }
    return "Hello World"
}

fun main() {
    if (someMethod() == null) error("Oops. Exiting.")
    val str: String = someMethod() // Future calls are smart-cast to non-nullable
}

Maybe someone with more Kotlin contracts experience can give some input?


It doesn’t matter if the map is read-only or not. Without capturing the value, calling map.get(key) (or any other method, including a final val’s getter), null can be swapped out from under your nose by the map implementation.
You’ll see people misunderstand this and request that non-local val properties be given the same smart cast for nulls.

1 Like

In essence what you are saying is that there is no guarantee ever that a value in a map is not null.
What i do not understand then is how the type inference can ever tell me its a non nullable value in the map.
The type inference in essence tells me that if the key exists the value must also exist. Hence my idea for a smartCast :slight_smile:
The smartcast is based upon the structure of the map not on the method that is being called (even if the jvm can in essence return null)

I do understand that you cannot effectively guarantee that a method call returns null or not in following calls. But to be clear its the datastructure that provides the boundry of not being able to add null values not the method.

Well maybe i just do not know enough about this stuff to be able to determine/see what is possible and what is not.

Yup–you and I can guess that it should not be null because we checked the key exists and the method returns something other than null. The compiler doesn’t know that especially since the map doesn’t even promise to return the same thing each call.
You’re asking for the compiler to know that one call to the map means something else about another call to the map.

Since there are many implementations of Map it wouldn’t make sense to assume all of them provide the guarantee of not returning null if the key exists. If we did assume this for all implementations of Map, the compiler would do invalided smart casts.

You might then ask that Kotlin give special treatment to some specific Map class so that the compiler assumes this guarantee for a specific implementation in the stdlib. This map couldn’t be Map or ImmutableMap since both of those are general interfaces–the current implementations weren’t built with the guarantee in mind.

Although this might allow some conveniences for a narrow use-case–instead we really need some mechanism for an author to make promises about their data structure. Or even make promises about properties as well as methods.

For example, as an author, I want clients of my LootBox data structure to get a smartcast of non-nullable values when the LootBox contains an item:

val box = LootBox("Sword")
if (box.value == null) exitProcess() // check by directly looking at the value
requireLootInBox(box) // check by some method that exits on no loot (aka, `requireNotNull(map[key])`)

// Here we get a nullable value even though we can assume from our check it should never be null.
val loot: String? = box.value 

Just like most maps, it’s pretty reasonable to assume our check of the data structure combined with our assumption of how the data structure works should be enough for a null check.

We need the author to provide some kind of guarantee that these special assumptions are correct. Contracts are how the author provides this special guarantees.

TL:DR
Just because you and I can determine the Map will probably only return non-null the compiler cannot. One reasonable way to fix this is to allow the author of the data structure to tell the compiler via a contract.


PS
Here are some fun examples that show how you can’t trust non-local (uncaptured values) to be null. What you’re asking for has been asked before but instead of map.get(key) they wanted it for property getters (which are just methods).

fun main(args: Array<String>) {
//sampleStart
    foo = true

    if (foo != null) {
        println("foo: $foo")
    }
//sampleEnd
}

var foo: Boolean? = true 
    get() = field.also {field = null}
fun main(args: Array<String>) {
//sampleStart
    foo = true

    if (foo != null) {
        println("foo: $foo")
        println("foo: $foo")
        println("foo: $foo")
    }
//sampleEnd
}

var foo: Boolean? = true 
    get() = when(field) {
        true -> { field = false; true }
        false -> { field = null; false }
        null -> { field = true; null }
    }
fun main() {
//sampleStart
    foo = true

    if (foo == false || foo == null || foo == true) {
        // ...
    } else {
        println("Foo is not true, false, or null! 🤔")
    }
//sampleEnd
}

var foo: Boolean? = true 
    get() = when(field) {
        true -> { field = false; true }
        false -> { field = null; false }
        null -> { field = true; null }
    }
fun main(args: Array<String>) {
//sampleStart
    if (foo != null) {
        doSomething()
        print("foo: $foo")
    }
//sampleEnd
}

var foo: Boolean? = true

fun doSomething() {
    // doSomething ...
    foo = null
}

Is it safe to say that the compiler does not have the type declaration of the map and thus cannot know what type of map is underhand?

Contracts sound interresting but is this not what we already do when we declare returntypes in methods? Same goes for the typedeclaration of a map. In essence we tell what can an cannot be in the map.

But all in all I get what the problem is now. :slight_smile:

Kind of. Knowing the exact type of map might help if that type could tell the compiler that it will never return null for a key checked by the user to not be null. But right now, AFAIK there isn’t a contract statement that allows you to tell the compiler that.

Exactly! :slight_smile: Method signatures are method contracts (in the normal sense. Not the same as “Kotlin Contracts”) and the generic type params also make up part of those contracts.

The only issue with Map is that even though we say it can only contain non-nullable values, the map interface’s get method says it will still return null if the key doesn’t exist. If only the compiler somehow knew we had already checked that key and also knew that the key won’t be changed.

Some things aren’t handled by method signatures though. For example, if we call a method that takes a lambda, can the compiler assume that lambda will be executed immediately and only once (which would allow using it to initialize write-once values)? Or maybe if the code returns at all, can the compiler know something about the parameters passed in (e.g. requireNotNull(param))Method signatures don’t cover that.

Awesome thanks for the explanation.

Glad to help :slight_smile:

Now that we’re on the same page, I’d like to double down on this request. I’d love to see some experimental additions to contracts that allow one to state promises on other methods.

For example, maybe something like this?

fun getValue(): String? = //...

fun requireValueWillNotBeNull() {
    contract {
        returns() implies (::getValue returns String)
    }
    // ...
}

I don’t see how this could be done but if it could you would be able to define an extension function to specify your map value exists:

fun Map<K, V>.requireKey(key: K) {
    contract {
        returns() implies (this::get.with(key) returns V) // Not sure how this could work at compile time..... 
    }
    // ...
}

^ still a leap since the underlying map type isn’t the one providing this promise… So not the best use case. You could limit this example’s scope to keep it from leaking out and being unsafe everywhere but maybe there’s better use cases to pull from like nullable properties. If we get ImmutableCollection interfaces then this example would make a lot more sense.
Cool that it would make modifying a method signature or property via contracts.

2 Likes