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

The Kotlin Type Hierarchy From Top to Bottom

Nat Pryce
October 04, 2018

The Kotlin Type Hierarchy From Top to Bottom

The Kotlin Type Hierarchy, from top to bottom (including the ins and outs of generics)

Coming from Java, Kotlin's type system can appear quite daunting: nullable types, Unit, Nothing, Any, variance specifiers, function types, and more. But don't panic! Kotlin's type hierarchy is actually simple – it has very few rules to learn and those rules combine together consistently and predictably. Thanks to those rules, Kotlin useful language features, such as null safety and unreachable code analysis, do not rely on special cases that you must memorise, or ad-hoc checks in the compiler and IDE that stop working when your code gets too complicated.

In this talk Nat will take you on a tour of Kotlin's type hierarchy and demonstrate how to use it to model your application domains, using example code taken from real projects.

Presented at KotlinConf 2018, Amsterdam.

Video: https://www.youtube.com/watch?v=juFkdMv4B9s

Nat Pryce

October 04, 2018
Tweet

More Decks by Nat Pryce

Other Decks in Programming

Transcript

  1. The Kotlin Type Hierarchy
    from top to bottom
    (including the ins and outs of generics)
    Nat Pryce
    @natpryce
    github.com/npryce
    speakerdeck.com/npryce

    View Slide

  2. ???

    View Slide

  3. View Slide

  4. JVM
    heap
    CAFE
    BABE
    ...
    foo() {
    ...
    }
    Types classify syntactic expressions, not runtime values
    fun f() {
    ...
    }
    101001
    011001
    011100
    1100...
    kotlinc
    class
    class
    Type Checker

    View Slide

  5. Types help you avoid common runtime errors
    https://rollbar.com/blog/top-10-javascript-errors/

    View Slide

  6. The same errors occur on the JVM due to reflection
    https://stackoverflow.com/questions/tagged/spring?sort=frequent

    View Slide

  7. There’s no trade off between type safety & clarity
    resources {
    journalCss methods {
    GET { _, journalId ->
    journalId.toPCode().asResultOr { JournalNotFound(journalId) }
    .flatMap { pCode -> storage.style(pCode) }
    .map { Response(OK).body(it.toCSS()).contentType(CSS_UTF_8).cacheable().allowedFromAnyOrigin() }
    .orElse { reportError(it, monitor) }
    }
    }
    journalLogo methods {
    GET { _, (journalId, format) ->
    journalId.toPCode().asResultOr { JournalNotFound(journalId) }
    .flatMap { pCode -> storage.logo(pCode, format) }
    .map { logoUri -> Response(FOUND).location(logoUri).cacheable() }
    .orElse { reportError(it, monitor) }
    }
    }
    partnerLogo methods {
    GET { _, partnerCode ->
    storage.partnerLogo(partnerCode)
    .onEachFailure { monitor.notify(MissingBrandingEvent(it)) }
    .flatMapFailure { storage.defaultPartnerLogo() }
    .map { logoBytes -> Response(OK).body(logoBytes).contentType(PNG).cacheable() }
    .orElse { reportError(it, monitor) }
    }
    }
    }
    http4k.org, github.com/npryce/krouton, github.com/npryce/result4k

    View Slide

  8. Kotlin’s type hierarchy from top to bottom

    View Slide

  9. Kotlin has built-in and user-defined types
    Logical Integral Floating Point Structured And these...
    Boolean Byte Float Array Any
    Char Double String Nothing
    Int Function types Unit
    Long
    Classes, interfaces and enums create user-defined types
    Function types, the “nullable” operator and generics compose types
    The Kotlin language defines some built-in types:

    View Slide

  10. Kotlin types are related by subtyping
    Here, Apple is a subtype of Fruit, Fruit is a supertype of Apple.
    Can be read as "An Apple is a Fruit".
    A subtype can be passed anywhere that expects its supertype.
    I can pass an Apple to code that expects a Fruit
    But not vice versa:
    I cannot pass arbitrary Fruit to code that expects only Apples
    Subtyping transitive
    An Apple is a Fruit, a Pippin is an Apple, therefore a Pippin is a Fruit

    View Slide

  11. Any is the ultimate supertype of all classes
    Any is the equivalent of Java's Object type
    Except: its values are never null

    View Slide

  12. Any is also the ultimate supertype of interfaces
    Note: the relationships between interface
    types is different to the relationships
    between classes exposed by reflection.

    View Slide

  13. Any has a few general operations
    Casting to a useful type sidesteps the type checker
    Beware of type erasure: not all casts can be fully checked at runtime
    Avoid unsafe casts by
    ● Programming to interfaces
    ● Sealed class hierarchies
    The compiler can’t help you if you use Any

    View Slide

  14. fun example() {
    println("Hello, KotlinConf")
    }
    Functions that only have side effects return Unit
    fun example(): Unit {
    println("Hello, KotlinConf")
    return Unit
    }
    package kotlin
    public object Unit {
    override fun toString() = "kotlin.Unit"
    }
    =

    View Slide

  15. Evaluating an expression of type Unit
    produces the singleton value Unit
    Evaluating an expression of type Nothing
    never produces a value at all.
    Expressions that do not produce a value have type Nothing

    View Slide

  16. We can include control flow in expressions
    Throw:
    val fruitPerGlass : Int = if (glassesSold > 0) orangesUsed/glassesSold
    else throw NoGlassesSold()
    Or even return:
    fun generateReport(orangesUsed: Int, glassesSold: Int): String {
    val fruitPerGlass : Int = if (glassesSold > 0) orangesUsed/glassesSold
    else return "no glasses sold"
    ...
    }

    View Slide

  17. We can write our own control flow statements
    The compiler's unreachable code analysis will understand them.
    inline fun forever(block: ()->Unit): Nothing {
    while(true) {
    block()
    }
    }
    forever {
    val m = receiveMessage()
    dispatchMessage(m)
    }
    println("finished") Unreachable code

    View Slide

  18. There is no way to declare that an expression cannot fail
    Just admitting the truth: a Kotlin program can, at any time:
    ● exhaust memory
    ● be terminated
    ● receive an unexpected signal
    ● have it’s thread interrupted
    ● overflow the stack
    ● have an incorrect classpath that causes a link error
    ● fail to load a class because of a hardware error
    ● ...

    View Slide

  19. Nulls and Nullability

    View Slide

  20. Nullable types form a parallel subtype hierarchy

    View Slide

  21. Nullability is itself a subtype relationship

    View Slide

  22. Beware: Any does not mean "any value"

    View Slide

  23. Unit can be nullable — it is not a special case like void

    View Slide

  24. Nothing? is the subtype of all nullable types

    View Slide

  25. val someJson = objectOf(
    "x" of 10,
    "y" of null
    )
    The null literal can cause overload ambiguity
    Error: overload ambiguity
    private val factory = JsonNodeFactory.instance
    fun objectOf(vararg properties: Pair) =
    factory.objectNode().apply {
    setAll(properties.toMap())
    }
    infix fun String.of(propertyValue: Int?) =
    Pair(this, factory.numberNode(propertyValue))
    infix fun String.of(propertyValue: String?) =
    Pair(this, factory.textNode(propertyValue))

    View Slide

  26. val someJson = objectOf(
    "x" of 10,
    "y" of null
    )
    Avoid ambiguity with explicit overloads for null literal
    private val factory = JsonNodeFactory.instance
    fun objectOf(vararg properties: Pair) =
    factory.objectNode().apply {
    setAll(properties.toMap())
    }
    infix fun String.of(propertyValue: Int?) =
    Pair(this, factory.numberNode(propertyValue))
    infix fun String.of(propertyValue: String?) =
    Pair(this, factory.textNode(propertyValue))
    infix fun String.of(propertyValue: Nothing?) =
    Pair(this, factory.nullNode())

    View Slide

  27. A generic type parameter T (no “?”) is nullable
    Type variables are nullable, even though they do not have a '?' suffix!
    E.g. the list created by this function may contain null.
    fun listOf(vararg items: T): List = ...
    Constrain a type parameter to be a subtype of Any to declare it non-null
    E.g. the following function guarantees its result has no null elements
    fun Iterable.filterNotNull(): List = ...

    View Slide

  28. The ins and outs of generics

    View Slide

  29. A generic is not a type, but a "function on types"
    interface Box {
    fun put(item: ITEM)
    fun take(): ITEM
    }
    Box Box
    Applying a generic to a type (or types) produces a type

    View Slide

  30. By default, generic types do not introduce subtyping
    interface Box {
    fun put(item: ITEM)
    fun take(): ITEM
    }
    Box Box

    View Slide

  31. Use "output" variance to identify what is returned
    interface Supplier {
    fun receive(): T
    }
    Fruit
    Supplier
    Oranges
    Supplier

    View Slide

  32. Use "input" variance to identify what is passed in
    interface Juicer {
    fun juice(solid: T): Liquid
    }
    Juicer Juicer

    View Slide

  33. Beware: input/output variance is not inferred
    … except for function types
    It's easy to make your generic types too restrictive. Be careful if publishing libraries.
    interface Hopper {
    fun takeOne(): T
    }
    class AutomaticJuicer(val source: Hopper)
    val apples: Hopper = ...
    val juicer = AutomaticJuicer(apples)

    View Slide

  34. Separate types for read, write & read/write
    interface Store {
    fun put(item: T)
    }
    interface Source {
    fun take(): T
    }
    interface Box: Store, Source
    Lots of examples in the standard library. E.g. List / MutableList

    View Slide

  35. Subtyping!

    View Slide

  36. The End
    Nat Pryce
    natpryce.com
    @natpryce
    github.com/npryce
    speakerdeck.com/npryce
    Libraries cited in this talk:
    http4k.org
    github.com/npryce/hamkrest
    github.com/npryce/krouton
    github.com/npryce/result4k
    Diagrams generated by PlantUML

    View Slide