Hello guys, I have been working on Android platfor...
# android
v
Hello guys, I have been working on Android platform for quite some time, last 3 years with Kotlin exclusively. I have come up with idea for improvement of the current contracts and I would like opinion of some more experienced people to see if it is worth doing. What I wanted to achieve is to pass block to a contract which would imply that condition is met inside that block context.
e.g. current solution for some validate function:
Copy code
fun main() {
    val p1: Request? = Request("a")
    val p2: Request? = null
    val p3: Request? = Request("b")

    if (validate(p1, p2)) {
        println(p1.arg)
        println(p2.arg)
    }

    if (validate(p1, p3)) {
        println(p1.arg)
        println(p3.arg)
    }
}

data class Request(val arg: String)


@OptIn(ExperimentalContracts::class)
private fun validate(p1: Request?, p2: Request?) : Boolean {
    contract {
        returns() implies (p1 != null && p2 != null)
    }

    return p1 != null && p2 != null
}
This is ok if developer knows how to use this validate function, but could be fatal if the developer is not aware of its implementation.
Copy code
fun main() {
    val p1: Request? = Request("a")
    val p2: Request? = null

    validate(p1, p2)
    println(p1.arg)
    println(p2.arg)
}
This piece of code would also make a smart cast, but would fail in runtime since p2 is null, that makes it dangerous.
Wanted behaviour of the contract (I envisioned), would be something like this:
Copy code
@OptIn(ExperimentalContracts::class)
private fun <T1, T2> validate(p1: T1?, p2: T2?, block: () -> Unit) : Boolean {
    contract {
        executes(block) implies (p1 is Number && p2 != null) 
    }
}
That would mean that this would work as expected:
Copy code
fun main() {
    val p1: Int? = 10
    val p2: String? = "string"

    validate(p1, p2) {
        println(p1) // p1 is smart casted
        println(p2) // p2 is smart casted
    }

    println(p1) // p1 is not smart casted
    println(p2) // p2 is not smart casted
}
g
The contract of the first example is wrong, you are saying that if the function returns, then the input is not null. It should be:
Copy code
returns(true) implies (p1 != null && p2 != null)
and with this change the smart cast should only happen inside of the if block, so your second snippet should not compile
v
@gmz that is correct, that was my mistake but that function still needs to be part of the if statement instead of the standalone behaviour shown in the third example, don't know if that is possible with the current contract behaviour
g
correct, but then it's not that different from using an if statement.
Copy code
if (validate(p1, p2)) {
    println(p1) // p1 is smart casted
    println(p2) // p2 is smart casted	
}
versus
Copy code
validate(p1, p2) {
    println(p1) // p1 is smart casted
    println(p2) // p2 is smart casted
}
It feels unnecessary, but that's my opinion. You could achieve something similar with a function such as this one:
Copy code
@OptIn(ExperimentalContracts::class)
private inline fun <T1 : Any, T2 : Any> validate(p1: T1?, p2: T2?, block: (p1: T1, p2: T2) -> Unit) {
    if (p1 != null && p2 != null) {
        block(p1, p2)
    }
}
I said similar because the usage changes a bit:
Copy code
validate(p1, p2) { p1, p2 ->
    println(p1) // p1 is smart casted
    println(p2) // p2 is smart casted
}
v
Yeah those are the options that we have right now, and it makes sense that example that I did in the thread looks very similar. But for some more complicated cases (something like state machine behaviour) would benefit from having smart casting in the executed block (maybe I am over-engineering things, but that is why I am asking here).
g
I'm probably not the right person to answer this, I just spotted the issue in your first example and chimed in. That said, I have an opinion on this. I see contracts as a way to propagate some extra information to some outer scope, which is something you cannot do without contracts. Your proposed solution allows to propagate the extra information to some inner scope, which you can already do, for example using a if statement. I'm all for convenient and helpful constructs that help reduce boilerplate, but the problem is that contracts are really powerful and misusing them can have catastrophic results. You are basically telling the compiler to trust you. Case in point, your first snippet. For this reason, I would personally stick with something that does not require the compiler to trust me when doable. But again, this is my opinion.
g
In the ticket they talk about inferred contracts. I think it would be cool to have something in between, and by that I mean an inferred contract for validation purposes at compile time, but always require an explicit contract. Now that I think of it, I see a warning for
InvocationKind
when I specify the wrong type. It would be cool to have even more validations and errors instead of warnings. That would make contracts a lot safer.