29

Smart Casts via Assertions + Kotlin Contracts - ProAndroidDev

 4 years ago
source link: https://proandroiddev.com/smart-casts-via-assertions-kotlin-contracts-fd737a92013b?gi=e52d6876ebdf
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Smart Casts via Assertions + Kotlin Contracts

Image for post
Image for post

There are three things I do every-time I start a codebase that I know that I will be maintaining for the long haul.

  1. Order a Rubber Duckie on Amazon
  2. Familiarize myself with the latest developments in the forever ongoing UI architecture wars (e.g. Mortar and Flow, Conductor, Scoop, and the newest challenger Jetpack Compose) before ultimately giving up and just relying on multiple Activities (yup, i’m that kind of Android dev).
  3. Set up an assertion framework

Enforcing Invariants through assertions

Invariants are some of the best ways to maintain your sanity as a coder. They are essentially unit tests that are always executing since they run during the execution of the production software and will save you a lot of debug time when used correctly. Don’t believe me, just ask NASA.

One of my favorite features in Kotlin is smart casts. As I was porting over assertion code from an old Java codebase that I had, something that kept bugging me about writing assertion code in Kotlin was the fact that after my assertion code was executed, there wasn’t any way for the kotlin compiler to infer the nullability (or lack thereof) of my instance variables. It would be ideal, if the Kotlin compiler allowed us to smart cast instance variables after the successful execution of assertion code.

For example, lets say I have a simple assertion function like:

fun String?.assertNotNull() {
if (this == null) {
throw AssertionError()
}
}

And in main, I simply call the assertion function

fun main() {
val str: String? = “”
str.assertNotNull()
println(“string length = ${str.length}”) // will not compile
}

Trying to access the str variable without the safe call operator will result in a compiler error. Unlike a safe cast within an if expression check for null, the compiler doesn’t know that we have guaranteed that str can never be null once the assertNotNull() function successfully returns.

Enter Kotlin Contracts…

We can use Kotlin Contracts to inform the compiler that we can guarantee certain properties about the parameters to the function based on how/what the function returns.

In this example, via a Contract Builder, we inform the compiler that if this function returns normally, the object that this extension function was called on is not null. We also need to any method that uses a Contract Builder as experimental. This will be explained in more detail later in the blog post.

@UseExperimental(ExperimentalContracts::class)
fun String?.assertNotNull() {
contract {
returns() implies (this@assertNotNull != null)
}
if (this == null){
throw AssertionError()
}
}

The compiler would then smart cast the variable, just as if we had performed a null check on it anytime after we call assertNotNull():

fun main() {
val str: String? = “”
str.assertNotNull()
println(“string length = ${str.length}”) // compiles just fine
}

Are Kotlin Contracts TOO Powerful?

Image for post
Image for post

Through the use of Kotlin Contracts, we are essentially trading compile time safety for succinct code. Through the use of Contract Builders, we are able to inform the compiler that certain built-in safety checks do not need to be performed because certain code paths will never execute. This works great when our Contract Builders are built correctly. When used incorrectly however, we have effectively removed vital compile time checks which can ultimately result in run-time errors (which will really ultimately result into panicked 2 a.m. slack messages from your PM).

As of now, Kotlin Contracts are experimental and have to be marked as such to be used.

@UseExperimental(ExperimentalContracts::class)
fun String?.assertNotNull() {
contract {
returns() implies (this@assertNotNull != null)
}
if (this == null){
throw AssertionError()
}
}

Which seems appropriate as this is a very powerful feature and could easily lead to production fires if not used appropriately, if Jet Brains ultimately decides to keep the feature in the language at all!

Conclusion

Nevertheless, now (within an experimental context) I am able to remove unnecessary null checks against variable types that have been verified by our hard fail assertion checks.

Hopefully soon Kotlin Contracts will lose their experimental designation because then the possibilities could truly be endless.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK