Contracts, lambdas and captured vals

I’ve been playing with contracts and wanted to confirm the behaviour I’m seeing. Suppose I have the function block defined below - I’m calling the lambda directly, but we should imagine that this will be called outside of this execution (but still exactly once), hence we aren’t making the function inline as lambda would need to be noinline anyway.

@UseExperimental(ExperimentalContracts::class)
fun block(lambda: () -> Unit) {
    contract {
        callsInPlace(lambda, InvocationKind.EXACTLY_ONCE)
    }
    lambda()
}

Because of the contract we are then able to define a val and capture and use it between block calls:

val list: List<Int>

block {
    list = listOf(1, 2, 3)
}

block {
    println(list.first()) // prints 1
}

However, curiously, if the block contains a lambda function that uses list, then it is no longer captured correctly, and instead you get a NullPointerException when trying to use it:

block {
    println(listOf(2, 3, 4).first { list.contains(it) }) // NullPointerException as list is null
}

This can be fixed by capturing the list first, however, you then, of course, get a warning about an unnecessary variable:

block {
    @Suppress("UnnecessaryVariable")
    val capturingList = list

    println(listOf(2, 3, 4).first { capturingList.contains(it) })
}

Is this behaviour by design? I am using Kotlin 1.3.72. Any way of getting a compiler warning for this kind of problem, or ensuring that if a capturing value isn’t marked as unnecessary?

1 Like

I think this part is where you get confused. If I understand you correctly in your real world example you would call the lambda at a later time, but exactly once.
This is breaking the contract of callsInPlace.

This contract specifies that:

  1. the function lambda can only be invoked during the call of the owner function, and it won’t be invoked after that owner function call is completed;
  2. (optionally) the function lambda is invoked the amount of times specified by the kind parameter, see the InvocationKind enum for possible values.

If you use callsInPlace it means that you must execute the lambda before your function exits.

Also you should note that the compiler can’t check that any contract is actually true. This means that you could write the code you have above, but as you pointed out it would lead to a NPE because you broke the contract.

1 Like

While it may be true that the OP was planning to do something that would violate the contract in a real setting, the actual example provided does in fact satisfy the contract. I see no reason why the given code shouldn’t work as expected. I think it is a bug.

import kotlin.contracts.*

@OptIn(ExperimentalContracts::class)
fun block(lambda: () -> Unit) {
    contract {
        callsInPlace(lambda, InvocationKind.EXACTLY_ONCE)
    }
    lambda()
}
fun main() {

    val list: List<Int>

    block {
        list = listOf(1, 2, 3)
    }
    
    block {
        println(listOf(2, 3, 4).first { list.contains(it) })
    }
}
2 Likes

I agree. I assumed that the code the OP was showing was just to illustrate and that the error occured in a more complex situation with the lambda called at a later point.
If the code fails with the code shown here this is definetly a bug and should be reported to https://kotl.in/issue

1 Like

Issue KT-38849 created.

1 Like

Ok, tryed it as well and your right it doesn’t work… Changing val list to a var seems to fix the issue. Something seems to be wrong with the way lambdas capture outside values.

1 Like

That’s certainly an interesting point about what the contract is actually specifying, and kinda means more often than not you may as well make the function inline.

1 Like