Kotlin Contracts: assert instance on reified type parameter
Asked Answered
O

4

8

I'm trying to write an assert function that checks if a given object is of a type T:

@UseExperimental(ExperimentalContracts::class)
inline fun <reified T> assertIsInstance(value: Any?) {
    contract {
        returns() implies (value is T)
    }

    Assertions.assertThat(value).isInstanceOf(T::class.java)
}

The function uses AssertJ to do the concrete assertion but I'm willing to let the compiler know that after its execution, the value is of type T so that a smartcast is possible. It seems like this does not work because:

Error in contract description: references to type parameters are forbidden in contracts

Is there another way to achieve this behavior? What's the issue here? Will this eventually be possible?

(Using Kotlin v1.3)

Overcharge answered 8/11, 2018 at 10:29 Comment(0)
G
3

At some point there were some (deeply technical) concerns regarding support of such constructions in an IDE, but it's possible that this limitation will be relaxed in the future.

Grum answered 8/11, 2018 at 11:40 Comment(2)
thanks for the response. Any alternative approach to implement such a contract?Overcharge
@Overcharge unfortunately, there's no way to work around it at the moment. You can track status this feature request here: youtrack.jetbrains.com/issue/KT-28298Grum
F
4

This has been bugging me for a couple hours, especially since this is possible:

val x: Any = "string"
require(x is String)
val len = x.length

The compiler is clearly able to understand these, so this is likely a limitation of the contracts themselves.

I've spent a while now trying to come up with some workarounds. For reference:

@UseExperimental(ExperimentalContracts::class)
inline fun <reified T> assertIsInstance(value: Any?) {
    contract {
        returns() implies T::class.isInstance(value))
    }
    if(value !is T){
        throw java.lang.IllegalArgumentException("Incorrect type");
    }
}

"Unsupported construct"

@UseExperimental(ExperimentalContracts::class)
inline fun <reified T> assertIsInstance(value: Any?, condition: Boolean = value is T) {
    contract {
        returns() implies condition
    }
    if(!condition)
        throw IllegalArgumentException("Incorrect type");
}

Compiles, but doesn't enable smart cast. The original motivation behind that one was placing a boolean in front of the contract, but contracts need to be the first part of a function, which made that impossible. You might as well remove the contract; it's useless in this case.

This was my last try:

@UseExperimental(ExperimentalContracts::class)
inline fun assertIsInstance(value: Any?, cls: KClass<out Any>) {
    contract {
        returns() implies (cls.isInstance(value))
    }
    if(!cls.isInstance(value))
        throw IllegalArgumentException("");
}

Another "unsupported construct".

Somehow I ended up with this:

@UseExperimental(ExperimentalContracts::class)
inline fun assertIsInstance(value: Any?) {
    contract {
        returns() implies (value.hashCode() == 0)
    }
    if(value.hashCode() != 0)
        throw java.lang.IllegalArgumentException();
}

But this gives a new error: only references to parameters are allowed in contract description.

TL;DR:

It doesn't look like you can. Sneaking it in like I did in the second example doesn't trigger smart cast, and the rest don't work due to various compiler errors.

At least for now, there doesn't appear to be a way. You could of course open an issue in the Kotlin repo and ask for something like this, but for now, it doesn't appear to be possible.

Finished answered 8/11, 2018 at 15:23 Comment(0)
G
3

At some point there were some (deeply technical) concerns regarding support of such constructions in an IDE, but it's possible that this limitation will be relaxed in the future.

Grum answered 8/11, 2018 at 11:40 Comment(2)
thanks for the response. Any alternative approach to implement such a contract?Overcharge
@Overcharge unfortunately, there's no way to work around it at the moment. You can track status this feature request here: youtrack.jetbrains.com/issue/KT-28298Grum
S
1

I have a solution that passes the my unit tests on Kotlin 1.8.20:

@OptIn(ExperimentalContracts::class)
@Suppress("UnusedPrivateMember")
public inline fun <K : KClass<V>, reified V> Any.checkIsInstance(kClass: K, lazyMessage: () -> Any): V {
    contract {
        returns() implies (this@checkIsInstance is V)
    }

    if (this !is V) {
        val message = lazyMessage()
        throw IllegalStateException(message.toString())
    } else {
        return this
    }
}

Edit: This can simplify to a version very similar to that in the original post, suggesting that the problem is now fixed in Kotlin:

@OptIn(ExperimentalContracts::class)
public inline infix fun <reified V> Any?.checkIsInstance(lazyMessage: () -> Any): V {
    contract {
        returns() implies (this@checkIsInstance is V)
    }

    if (this !is V) {
        val message = lazyMessage()
        throw IllegalStateException(message.toString())
    } else {
        return this
    }
}
Shipmaster answered 20/4, 2023 at 10:11 Comment(1)
This indeed works for me. ``` fun t() { val s: String = ("" as Any).checkIsInstance<String> {""} } ```Shannon
P
0

Doesn't the as operator do this?

fun main() {
    val x: Any = "string"
    x as String
    val len = x.length
    println(len)
}

https://pl.kotl.in/uFCsGWEZm

Parathyroid answered 3/7, 2019 at 8:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.