The Kotlin Type Hierarchy From Top to Bottom

5358df52bd2ef4f57da1b1cc8634cfd9?s=47 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

5358df52bd2ef4f57da1b1cc8634cfd9?s=128

Nat Pryce

October 04, 2018
Tweet

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
  2. ???

  3. None
  4. JVM heap CAFE BABE ... foo() { ... } Types

    classify syntactic expressions, not runtime values fun f() { ... } 101001 011001 011100 1100... kotlinc class class Type Checker
  5. Types help you avoid common runtime errors https://rollbar.com/blog/top-10-javascript-errors/

  6. The same errors occur on the JVM due to reflection

    https://stackoverflow.com/questions/tagged/spring?sort=frequent
  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
  8. Kotlin’s type hierarchy from top to bottom

  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:
  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
  11. Any is the ultimate supertype of all classes Any is

    the equivalent of Java's Object type Except: its values are never null
  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.
  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
  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" } =
  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
  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" ... }
  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
  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 • ...
  19. Nulls and Nullability

  20. Nullable types form a parallel subtype hierarchy

  21. Nullability is itself a subtype relationship

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

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

    case like void
  24. Nothing? is the subtype of all nullable types

  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<String, JsonNode>) = 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))
  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<String, JsonNode>) = 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())
  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 <T> listOf(vararg items: T): List<T> = ... 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 <T : Any> Iterable<T?>.filterNotNull(): List<T> = ...
  28. The ins and outs of generics

  29. A generic is not a type, but a "function on

    types" interface Box<ITEM> { fun put(item: ITEM) fun take(): ITEM } Box<Fruit> Box<Orange> Applying a generic to a type (or types) produces a type
  30. By default, generic types do not introduce subtyping interface Box<ITEM>

    { fun put(item: ITEM) fun take(): ITEM } Box<Fruit> Box<Orange>
  31. Use "output" variance to identify what is returned interface Supplier<out

    T> { fun receive(): T } Fruit Supplier<Fruit> Oranges Supplier<Orange>
  32. Use "input" variance to identify what is passed in interface

    Juicer<in T> { fun juice(solid: T): Liquid } Juicer<Fruit> Juicer<Orange>
  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<T> { fun takeOne(): T } class AutomaticJuicer(val source: Hopper<Fruit>) val apples: Hopper<Apple> = ... val juicer = AutomaticJuicer(apples)
  34. Separate types for read, write & read/write interface Store<in T>

    { fun put(item: T) } interface Source<out T> { fun take(): T } interface Box<T>: Store<T>, Source<T> Lots of examples in the standard library. E.g. List<T> / MutableList<T>
  35. Subtyping!

  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