Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Contracts and Inline classes

Contracts and Inline classes

Kotlin contracts and inline classes

Dan Rusu

May 22, 2019
Tweet

More Decks by Dan Rusu

Other Decks in Programming

Transcript

  1. FR Refreshments • Provided by Faire • We use Kotlin

    for backend and Android development 2
  2. FR Contracts • Introduced in Kotlin 1.3 (currently experimental) •

    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" } } }
  3. FR Assigning val from lambda 4 inline fun logAndRun(action: String,

    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() } … }
  4. FR Assigning val from lambda (cont’d) 5 inline fun logAndRun(action:

    String, run: () -> Unit) { contract { callsInPlace(run, InvocationKind.EXACTLY_ONCE) } logger.info(“starting $action”) run() } fun setup() { val connection: Connection logAndRun(“connection setup”) { // Initialization is now valid connection = establishConnection() } … }
  5. FR Assigning var from lambda 6 inline fun autoRetry(run: ()

    -> Unit) { var numTries = 0 while(numTries <= MAX_RETRIES) { try { run() return } catch (e: Exception) { numTries++ if (numTries > MAX_RETRIES) throw e } } } fun generateToken(prefix: String) { var token: String autoRetry { token = prefix + Random.nextInt(0, 100000) validateUniqueToken(token) } // Compiler error: token must be initialized println(“Generated token: $token”) }
  6. FR Assigning var from lambda (cont’d) 7 inline fun autoRetry(run:

    () -> Unit) { contract { callsInPlace(run, InvocationKind.AT_LEAST_ONCE) } … // auto retry logic from previous slide run() } fun generateToken(prefix: String) { var token: String autoRetry { token = prefix + Random.nextInt(0, 100000) validateUniqueToken(token) } // Compiler knows that token is initialized println(“Generated token: $token”) }
  7. FR Smart cast with nullable types 8 data class UserRequest(val

    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) }
  8. FR Smart cast with nullable types (cont’d) 9 fun createUser(request:

    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") } }
  9. FR Smart cast with nullable types (cont’d) 10 fun createUser(request:

    UserRequest): User { validateUserRequest(request.userName, request.isManager) return User(request.userName, request.isManager) } fun validateUserRequest(userName: String?, isManager: Boolean?) { contract { returns() implies (userName != null && isManager != null) } if (userName == null || userName.isBlank()) { throw IllegalArgumentException("Missing User Name") } if (isManager == null) { throw IllegalArgumentException("Unknown if Master or Code Monkey") } }
  10. FR Smart cast to instance type 11 open class SessionUser(var

    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 }
  11. FR Smart cast to instance type (cont’d) 12 fun SessionUser.isActiveUser():

    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 }
  12. FR Multiple smart casts 13 fun SessionUser.isUser(): Boolean { contract

    { 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 }
  13. FR Inline Classes • Introduced in Kotlin 1.3 (currently experimental)

    • Zero-cost abstraction • Safer and more robust code without sacrificing performance / scalability • Can be enabled in Gradle using: 14 compileKotlin { kotlinOptions { freeCompilerArgs += "-XXLanguage:+InlineClasses" } }
  14. FR Motivation • In 1999, after 10 months of travel

    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
  15. FR Nasa Orbiter 16 inline class Meter(val value: Double) inline

    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)) … }
  16. FR Validation 17 inline class Age @Deprecated("Don't call the constructor

    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
  17. FR State Transition Enforcement 18 class Person(val name: String) inline

    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
  18. FR Summary • Compiled to underlying type • Can store

    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