The goal of contracts is to give semantic information to the compiler • Enables extra patterns • Reduces checks and casts in the codebase • Can be enabled in Gradle using: 3 compileKotlin { kotlinOptions { freeCompilerArgs { "-Xuse-experimental=kotlin.contracts.ExperimentalContracts" } } }
run: () -> Unit) { logger.info(“starting $action”) run() } fun setup() { val connection: Connection logAndRun(“connection setup”) { // Does not compile: // Initialization forbidden due to possible reassignment connection = establishConnection() } … }
userName: String?, val isManager: Boolean?) data class User(val userName: String, val isManager: Boolean) fun createUser(request: UserRequest): User { if (request.userName == null || request.userName.isBlank()) { throw IllegalArgumentException("Missing User Name") } if (request.isManager == null ) { throw IllegalArgumentException("Unknown if Manager or Code Monkey") } // userName and isManager is smart cast to non-null types return User(request.userName, request.isManager) }
UserRequest): User { validateUserRequest(request) // Does not compile // userName and isManager no longer smart cast to non-null types return User(request.userName, request.isManager) } fun validateUserRequest(request: UserRequest) { if (request.userName == null || request.userName.isBlank()) { throw IllegalArgumentException("Missing User Name") } if (request.isManager == null) { throw IllegalArgumentException("Unknown if Manager or Code Monkey") } }
isActive: Boolean = true) data class Robot(val robotName: String) : SessionUser() data class User(val userName: String, val isManager: Boolean) : SessionUser() { fun isPrivileged(): Boolean = isManager } fun SessionUser.isActiveUser(): Boolean = this is User && this.isActive fun isPrivilegedSession(sessionUser: SessionUser): Boolean { if (sessionUser.isActiveUser()) { // Note isPrivileged only exists on User type return (sessionUser as User).isPrivileged() } return false }
Boolean { contract { returns(true) implies (this@isActiveUser is User) } return this is User && this.isActive } fun isPrivilegedSession(sessionUser: SessionUser): Boolean { if (sessionUser.isActiveUser()) { // Note sessionUser smart cast to User type return sessionUser.isPrivileged() } return false }
{ returns(true) implies (this@isUser is User) returns(false) implies (this@isUser is Robot) } return this is User } fun isPrivilegedSession(sessionUser: SessionUser): Boolean { if (sessionUser.isUser()) { return sessionUser.isPrivileged() // smart cast to User type } else { sessionUser.terminate() // smart cast to Robot type } return false }
to Mars, the $125 million Nasa orbiter burned and broke into pieces. Caused by accidentally mixing imperial and metric units • Wrapping values in hot sections of code can have severe impacts on real-time latency-sensitive requirements • We need stronger types that are enforced at compile time without introducing additional memory, performance, or scalability concerns • We need to reason about code and make assumptions: • E.g. Age has already been validated so a negative value won’t accidentally sneak through • E.g. Length is in meters • E.g. Id refers to a customer (it’s not just any number or an id for a different type) • E.g. Unsigned types make more sense in some areas such as distances 15
class Foot(val value: Double) val Int.meters get() = Meter(toDouble()) val Int.feet get() = Foot(toDouble()) fun shouldFireReverseThrusters(distanceRemaining: Meter): Boolean { TODO() } fun visitMars() { // Does not compile. Type mismatch, required Meter, found Foot if (shouldFireReverseThrusters(5000.feet)) … // Allowed if (shouldFireReverseThrusters(5000.meters)) … }
directly", ReplaceWith("value.toAge()")) constructor(val value: Int) fun Int.toAge(): Age { require(this in 1..100) { "Age $this is out of range"} @Suppress("DEPRECATION") return Age(this) } operator fun Int.minus(age: Age) = this - age.value // Formula is safe since we know that age will never be negative fun computeRations(age: Age): Int = 100 - age
class SittingPerson(private val person: Person) { fun walk() = WalkingPerson(person) } inline class WalkingPerson(private val person: Person) { fun sit() = SittingPerson(person) fun startRunning() = RunningPerson(person) } inline class RunningPerson(private val person: Person) { fun walk() = WalkingPerson(person) } fun createPerson() = SittingPerson(Person("Dan")) … var person = createPerson() person.startRunning() // Does not compile, need to walk before you can run person.walk().startRunning().walk().sit() // Allowed
a single value • No backing fields (no delegation e.g. lateinit) • No initializer block • Can be addressed by deprecating constructor and creating a single creation function • Cannot participate in class hierarchy • But can implement interfaces • Cannot be used from Java • Are sometimes auto-boxed (e.g. when storing in collections) • Referential equality is prohibited due to this 19