Kotlin Contracts not working for null-check in extension function
Asked Answered
C

3

11

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
  }
Chapin answered 19/1, 2023 at 21:6 Comment(3)
Your example seems to work in Kotlin Playground. What version of Kotlin are you using? Are you targeting Kotlin/JVM, or something else?Describe
I am on Kotlin 1.8.20 and see the same error as OPKennykeno
Have you tried declaring extension function inline? That's way code that is calling isValidID would be able to "see" this@isValidID and optimize accordingly. Now this implication of non-nullability doesn't leave the body of the function.Amadeo
R
5

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!

Romanov answered 1/3 at 10:0 Comment(1)
So, I guess this is one of the rare cases where we can definitely say "It must be a compiler bug" ;)Furan
P
2

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.

Peipeiffer answered 1/3 at 11:50 Comment(0)
C
0

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))
    }
}
Calabar answered 17/1 at 13:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.