Kotlin Contracts not working for null-check in extension function

534 Views Asked by At

I'm trying to write an extension function that returns true if the value is not null or 0 and use a contract to guarantee to the compiler that if I return true, the value is non-null. However, it doesn't seem to be working with smart casting. It still won't compile when I try to pass the value into a function that takes a non-nullable Long.

I tried to compile this code and it would not work. I expected the id to be smart-casted to a Long from a Long? since the contract guarantees that if isValidId returns true then the passed in Long? is not null.

As you can see, the property is immutable, so I don't think that's the issue. I also added some more code below, because the problem appears to be specific to extension functions. It works when I pass ID as a traditional parameter.

fun test() {
    val id: Long? = null //5L
    if (id.isValidID()) {
      // It won't compile because the compiler says that id is a Long?
      // instead of smart casting it to a Long. doWithId requires a Long.
      doWithId(id) 
    }
  }

  fun doWithId(id: Long) {}

  @OptIn(ExperimentalContracts::class)
  fun Long?.isValidID(): Boolean {
    contract { returns(true) implies (this@isValidID != null) }
    return this != null && this != 0L
  }

Thanks!

EDIT: Oh! It works when it's not an extension function. Does anybody know how I can make extension functions work?

fun test() {
    val id: Long? = null
    if (isValidID(id)) {
      // This compiles now that I pass ID as a param instead of
      // passing it in an extension function.
      doWithId(id)
    }
  }

  fun doWithId(id: Long) {}

  @OptIn(ExperimentalContracts::class)
  fun isValidID(id: Long?): Boolean {
    contract { returns(true) implies (id != null) }
    return id != null && id != 0L
  }
3

There are 3 best solutions below

0
On

I think I understand what is happening. The reason why this doesn't work with the extension function for you has more to do with how the compiler interprets the code than with the extension function itself. What I am assuming is that when the compiler looks at the extension function, it only sees this:

    if (id.isValidID()) {
        doWithId(id)
    }

We could say that this way we can be sure that id is not null. However, the compiler probably only sees this as a call to a function that has no predictable effect on determining if the value of id is null or not. And so the compiler cannot make any judgement on if id is null or not.

In the traditional case the compiler sees this:

    if (isValidID(id)) {
        doWithId(id)
    }

And this for the compiler will result in a validation only if the isValidID function results in true, but this way, the compiler also sees that the return value depends on evaluating that the id is not null. This way, it can perform directly the smartcast from Long? to Long.

It is very semantic but it matches with I expect that the compiler does. I do not know if the compiler will change this anytime soon, but it is also true that extension functions are only around for so long and so to solve this in the meantime we can use something like this:

fun test() {
    val id: Long? = 5L
    if (id.isValidID()) {
        doWithId(id!!)
    }
}

but I always prefer not to use !! like this:

fun test() {
    val id: Long? = 5L
    if (id.isValidID()) {
        doWithId(requireNotNull(id))
    }
}
1
On

TLDR: Define Long?.isValidID() as a top level function.


I ran in the same problem, and it seems it has something to do where you define the contract-aware-extension-function. I don't understand exactly why, but as @aSemy mentioned in his answer: "Your example seems to work in Kotlin Playground".

When I defined the function within my class, the compiler did not smart cast; notice this does works though for normal non-extension-contract-aware-functions. But when I moved the function to top level, it did compile!

0
On

As Jacob says, it works if you make it a top-level function.

If you have it as a class member, it works in 2.0.0 but not in 1.9 or earlier.