Can I use a Kotlin Contract to guarantee the non-nullity of another property?

1.1k Views Asked by At

I have a class with a nullable property description: String?, and for convenience would like to expose a fun hasDescription(): Boolean that null-checks said String.

For even more convenience, I am hoping to use a contract so I can smart cast description to non-nullable String after checking hasDescription().

  1. Is this possible now (with experimental contracts, and Kotlin 1.5)?
  2. Might it be possible when contracts go stable?
data class Widget (
    val description: String? = null,
)


class Gadget {
    val widget: Widget? = null

    val description: String? get() = widget?.description

    @ExperimentalContracts
    fun hasDescription(): Boolean {
        // Error in contract description: only references to
        // parameters are allowed in contract description
        contract {
            returns(true) implies((this@Gadget).description is String)
        }

        return this.description != null
    }
}

@ExperimentalContracts
fun testContract(gadget: Gadget) {
    if (gadget.hasDescription()) {
        // Error: Type mismatch (String? for String)
        // expected, since the contract itself does not compile
        val description: String = gadget.description
    }
}
1

There are 1 best solutions below

0
On

In short, it's not possible.

Contracts primarily consider the behavior of methods rather than the properties of values. Properties of values should be handled by the type system rather than contracts.

And yes, it might be implemented in the future.

It doesn't mean that if something potentially can be handled by the type system, then it is out of contracts' scope.

https://github.com/Kotlin/KEEP/blob/master/proposals/kotlin-contracts.md#scope-and-restrictions

Alternatives

Nulls are well handled in Kotlin's type system, and so we can use that to check for nullability.

Smart casting

Using smart-casting would remove the need for Contracts and the hasDescription() method.

fun nullCheckWhen(gadget: Gadget) {
  when (val description = gadget.description) {
    null -> println("description is null")
    else -> println("description.length ${description.length}")
  }
}
fun earlyReturn(gadget: Gadget) {
  val description = gadget.description ?: return

  println("description.length ${description.length}")
}
fun forcefulNullCheck(gadget: Gadget) {

  val description = requireNotNull(gadget.description) {
    "description must not be null"
  }

  println("description.length ${description.length}")
}
fun elvisScopeFunctionNullSafe(gadget: Gadget) {

  gadget.widget?.description?.let { description ->
    println("description.length ${description.length}")
  }

}

These are a little more clunky, and not as pretty as Contracts could be - but at least they work.

In fact, some of them use Kotlin Contracts, but of course they can only work on method parameters.

Preconditions.kt

/**
 * Throws an [IllegalArgumentException] if the [value] is null. Otherwise returns the not null value.
 */
@kotlin.internal.InlineOnly
public inline fun <T : Any> requireNotNull(value: T?): T {
  contract {
    returns() implies (value != null)
  }
  return requireNotNull(value) { "Required value was null." }
}

Type hierarchies

I don't think this approach is applicable to your situation, but I thought I'd share it as it can be useful.

If description was defined in an interface as a String?, then Widget could implement description as a non-nullable String. This works because in Kotlin the nullable type is a super-type of the non-nullable type. Any implementation of description can choose to be either be String? or String.

interface Component {
  val description: String?
}


data class Widget(
  override val description: String
) : Component


data class Greeble(
  override val description: String?
) : Component


fun nullCheckGeneric(component: Component) {

  when (component) {
    is Widget  ->
      // no need for a null check
      println("description.length ${component.description.length}")
    is Greeble ->
      // description is nullable
      println("description.length ${component.description?.length}")
  }

}