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

Devoxx Belgium 2024 - Kotlin 2.0 and beyond

Devoxx Belgium 2024 - Kotlin 2.0 and beyond

Kotlin 2.0, released in May 2024, marked a significant milestone in the language's evolution. At the heart of this major version lies the new compiler front-end, codenamed K2. The release delivered better performance and stabilization of the language features across multiple compilation targets. Despite being a major release, Kotlin 2.0 prioritized a smooth migration path, focusing on refinement rather than introducing drastic changes.

We will first take a look at improvements in Kotlin 2.0 release, highlighting the introduction of frontend intermediate representation (FIR) and the new control flow engine.

Then, we'll shift our focus to the horizon, and discuss the new ideas on Kotlin's roadmap:
* Guard conditions - enhancing control flow and null safety
* Context parameters - improving code organization
* Union types for errors - bringing more expressiveness to type systems
* Named-based destructuring - for better readability and reducing errors
* Contracts - enabling more precise static analysis (!!!!!)

Whether you're a seasoned Kotlin developer or just starting out, this talk promises to expand your understanding of the language.

Anton Arhipov

October 10, 2024
Tweet

More Decks by Anton Arhipov

Other Decks in Programming

Transcript

  1. July 20, 2011 - Kotlin announced at JVMLS February 15,

    2016 - Kotlin 1.0 May 21, 2024 - Kotlin 2.0
  2. July 20, 2011 - Kotlin announced at JVMLS February 15,

    2016 - Kotlin 1.0 May 21, 2024 - Kotlin 2.0 "A general purpose, statically typed, object-oriented alternative JVM programming language with type inference"
  3. July 20, 2011 - Kotlin announced at JVMLS February 15,

    2016 - Kotlin 1.0 May 21, 2024 - Kotlin 2.0
  4. July 20, 2011 - Kotlin announced at JVMLS February 15,

    2016 - Kotlin 1.0 2020 - Anton joined Kotlin team
  5. July 20, 2011 - Kotlin announced at JVMLS February 15,

    2016 - Kotlin 1.0 2020 ANTON KOTLIN FANBOYS Reality ANTON JAVA DEVELOPERS Expectation Me becoming Kotlin advocate:
  6. data class Person( val name: String, val age: Int )

    It used to be very easy to create the wow e ff ect with this: public class Person { private final String name; private final int age; public Person(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public int getAge() { return age; } @Override public boolean equals(Object o) { if (this = = o) return true; if (o == null | | getClass() ! = o.getClass()) return false; Person person = (Person) o; if (age != person.age) return false; return name != null ? name.equals(person.name) : person.name == null; } @Override public int hashCode() { int result = name != null ? name.hashCode() : 0; result = 31 * result + age; return result; } @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; } }
  7. data class Person( val name: String, val age: Int )

    But now Java has records... public record Person( String name, int age ) {}
  8. fun main() { val event = "Devoxx" println("Hello, ${event.uppercase()}!") println("Hello,

    ${event.randomCase()}!") } fun String.randomCase(chance: Double = 0.5): String { return map { if (Math.random() < chance) it.uppercaseChar() else it.lowercaseChar() }.joinToString("") } Top-level functions
  9. fun main() { val event = "Devoxx" println("Hello, ${event.uppercase()}!") println("Hello,

    ${event.randomCase()}!") } fun String.randomCase(chance: Double = 0.5): String { return map { if (Math.random() < chance) it.uppercaseChar() else it.lowercaseChar() }.joinToString("") } Extension functions!
  10. fun main() { val event = "Devoxx" println("Hello, ${event.uppercase()}!") println("Hello,

    ${event.randomCase()}!") } fun String.randomCase(chance: Double = 0.5): String = map { if (Math.random() < chance) it.uppercaseChar() else it.lowercaseChar() }.joinToString("") } Single-expression functions!!
  11. fun main() { val event = "Devoxx" println("Hello, ${event.uppercase()}!") println("Hello,

    ${event.randomCase()}!") } fun String.randomCase(chance: Double = 0.5) = map { if (Math.random() < chance) it.uppercaseChar() else it.lowercaseChar() }.joinToString("") } Type inference!!!
  12. fun main() { val event = "Devoxx" println("Hello, ${event.uppercase()}!") println("Hello,

    ${event.randomCase()}!") } fun String.randomCase(chance: Double = 0.5) = map { if (Math.random() < chance) it.uppercaseChar() else it.lowercaseChar() }.joinToString("") } Default argument values
  13. fun main() { val event = "Devoxx" println("Hello, ${event.uppercase()}!") println("Hello,

    ${event.randomCase(offset = 0.25)}!") } fun String.randomCase(chance: Double = 0.5, offset: Double = 0.1) = map { if (Math.random() < chance) it.uppercaseChar() else it.lowercaseChar() }.joinToString("") } Named parameters
  14. fun main() { val event = "Devoxx" println("Hello, ${event.uppercase()}!") println("Hello,

    ${event.randomCase(offset = 0.25)}!") } fun String.randomCase(chance: Double = 0.5, offset: Double = 0.1) = map { if (Math.random() < chance) it.uppercaseChar() else it.lowercaseChar() }.joinToString("") } Named parameters
  15. fun main() { val event = "Devoxx" println("Hello, ${event.uppercase()}!") println("Hello,

    ${event.randomCase(offset = 0.25)}!") println("Hello, ${event.transform { it.randomCase(offset = 0.25) }} !") } fun String.transform(transformer: (String) -> String) = transformer(this) Trailing lambda as a parameter
  16. fun main() { val event: String? = getEventName() println("Hello, ${event.uppercase()}!")

    println("Hello, ${event.randomCase(offset = 0.25)}!") } private fun getEventName(): String? = "Devoxx" Nullable types
  17. fun main() { val event: String? = getEventName() println("Hello, ${event

    ? . uppercase()}!") println("Hello, ${event ! ! .randomCase(offset = 0.25)}!") } private fun getEventName(): String? = "Devoxx" Operators for working with null values
  18. Kotlin still has a lot to o ff er: These

    features combined make a huge di ff erence in how we reason about the code and structure Kotlin programs
  19. What is Kotlin today? Kotlin is a language, but not

    only Kotlin/JVM, Kotlin/Native, Kotlin/JS, Kotlin/Wasm
  20. What is Kotlin today? Kotlin is a language, but not

    only Kotlin/JVM, Kotlin/Native, Kotlin/JS, Kotlin/Wasm Gradle Kotlin DSL, Kotlin Scripting
  21. What is Kotlin today? Kotlin is a language, but not

    only Kotlin/JVM, Kotlin/Native, Kotlin/JS, Kotlin/Wasm Gradle Kotlin DSL, Kotlin Scripting Kotlin for Android & Server side
  22. What is Kotlin today? Kotlin is a language, but not

    only Kotlin/JVM, Kotlin/Native, Kotlin/JS, Kotlin/Wasm Gradle Kotlin DSL, Kotlin Scripting Kotlin for Android & Server side Tooling: IntelliJ IDEA, Android Studio extension, Fleet
  23. What is Kotlin today? Kotlin is a language, but not

    only Kotlin/JVM, Kotlin/Native, Kotlin/JS, Kotlin/Wasm Gradle Kotlin DSL, Kotlin Scripting Kotlin for Android & Server side Tooling: IntelliJ IDEA, Android Studio extension, Fleet KotlinX libraries: coroutines, serialization, datetime, io
  24. What is Kotlin today? Kotlin is a language, but not

    only Kotlin/JVM, Kotlin/Native, Kotlin/JS, Kotlin/Wasm Gradle Kotlin DSL, Kotlin Scripting Kotlin for Android & Server side Tooling: IntelliJ IDEA, Android Studio extension, Fleet KotlinX libraries: coroutines, serialization, datetime, io Compiler Tooling: Compose, KSP, Power Assert, Arrow plugins…
  25. July 20, 2011 - Kotlin announced at JVMLS February 15,

    2016 - Kotlin 1.0 May 21, 2024 - Kotlin 2.0
  26. Features added after Kotlin 1.0: - Multiplatform projects - Coroutines

    - Inline / Value classes - Trailing comma - fun interfaces - ...
  27. Features added after Kotlin 1.0: - Multiplatform projects - Coroutines

    - Inline / Value classes - Trailing comma - fun interfaces - Type aliases - Sealed classes & interfaces - Contracts - break/continue inside when - Exhaustive when statements - Builder inference - . . < operator - Data objects
  28. Features added after Kotlin 1.0: - Multiplatform projects - Coroutines

    - Inline / Value classes - Trailing comma - fun interfaces - Type aliases - Sealed classes & interfaces - Contracts - break/continue inside when - Exhaustive when statements - Builder inference - ..< operator - Data objects - provideDelegate - Bound callable references - Destructuring in lambdas - Array literals in annotations - Local lateinit variables - Opt-in annotations - De fi nitely non-nullable types - Instantiation of annotation classes - Support for JSpecify - suspend functions as supertypes - Secondary constructors for inline value classes
  29. K2: The new Kotlin compiler - why? 1. A few

    language features have appeared unexpectedly in Kotlin Hard to maintain and evolve the compiler 2. Interaction with compiler and IDEs Many ad-hoc solutions, no strict contracts, and no stable API 3. Compilation time performance
  30. K2: The new Kotlin compiler - why? 1. A few

    language features have appeared unexpectedly in Kotlin Hard to maintain and evolve the compiler 2. Interaction with compiler and IDEs Many ad-hoc solutions, no strict contracts, and no stable API 3. Compilation time performance
  31. K2: The new Kotlin compiler - why? 1. A few

    language features have appeared unexpectedly in Kotlin Hard to maintain and evolve the compiler 2. Interaction with compiler and IDEs Many ad-hoc solutions, no strict contracts, and no stable API 3. Compilation time performance
  32. Kotlin 2.0 More than 80 features in the di ff

    erent subsystems Around 25 and small improvements within the language Main focus is on correctness and performance
  33. if (condition) { println("Hello") } when { condition -> println("Hello")

    } for (n in list) { println(n) } val <interator> = list.interator() while(<iterator>.hasNext()){ val s = <iterator>.next() println(s) } Frontend Intermediate Representation (FIR)
  34. if (condition) { println("Hello") } when { condition -> println("Hello")

    } for (n in list) { println(n) } val <interator> = list.interator() while(<iterator>.hasNext()){ val s = <iterator>.next() println(s) } val (a, b) = "a" to "b" val <pair> = "a" to "b" val a = pair.component1() val b = pair.component2() Frontend Intermediate Representation (FIR)
  35. fun mutate(ml: MutableList<Long>) { ml[0] = ml[0] + 1 }

    Combination of Long and Integer Literal Types Frontend Intermediate Representation (FIR)
  36. fun mutate(ml: MutableList<Long>) { ml[0] = ml[0] + 1 }

    Combination of Long and Integer Literal Types Long Integer Literal Type Frontend Intermediate Representation (FIR)
  37. fun mutate(ml: MutableList<Long>) { ml[0] += 1 } Combination of

    Long and Integer Literal Types Error: 1L is required // Error in Kotlin 1.x Frontend Intermediate Representation (FIR)
  38. fun mutate(ml: MutableList<Long>) { ml[0] += 1 } Combination of

    Long and Integer Literal Types // OK in 2.0 Desugared into: ml.set(0, ml.get(0).plus(1)) Frontend Intermediate Representation (FIR)
  39. Combination of nullable operator-calls class Box(val ml: MutableList<Long>) fun mutate(box:

    Box?) { box ?. ml[0] += 1 // Error in 1.x box ?. ml[0] += 1L // Error in 1.x } Frontend Intermediate Representation (FIR)
  40. Combination of nullable operator-calls class Box(val ml: MutableList<Long>) fun mutate(box:

    Box?) { box ?. ml[0] += 1 // OK in 2.0 } box ?. run { ml.set(0, ml.get(0).plus(1))} Desugared into: Frontend Intermediate Representation (FIR)
  41. New control fl ow engine read: more smart-casts! - KT-7186

    Smart cast for captured variables inside changing closures of inline functions - KT-4113 Smart casts for properties to not-null functional types at invoke calls - KT-25747 DFA variables: propagate smart cast results from local variables - KT-1982 Smart cast to a common supertype of subject types after || (OR operator) - ...
  42. class Cat { fun purr() { println("Purr purr") } }

    fun petAnimal(animal: Any) { if (animal is Cat) { animal.purr() } } Smart-casts
  43. class Cat { fun purr() { println("Purr purr") } }

    fun petAnimal(animal: Any) { if (animal is Cat) { animal.purr() } } Smart-casts
  44. class Cat { fun purr() { println("Purr purr") } }

    fun petAnimal(animal: Any) { val isCat = animal is Cat if (isCat) { animal.purr() // Error in Kotlin 1.x } } Smart-casts from variables
  45. class Cat { fun purr() { println("Purr purr") } }

    fun petAnimal(animal: Any) { val isCat = animal is Cat if (isCat) { animal.purr() // Error in Kotlin 1.x } } Smart-casts from variables Kotlin 1.x: variables don't carry any data fl ow information
  46. class Cat { fun purr() { println("Purr purr") } }

    fun petAnimal(animal: Any) { val isCat = animal is Cat if (isCat) { animal.purr() // OK in Kotlin 2.0 } } Smart-casts from variables Kotlin 2.0: synthetic data fl ow variables propagate information about smart-casts
  47. Smart-casts from variables class Card(val holder: String?) fun findHolder(card: Any):

    String { val cardWithHolder = card is Card && !card.holder.isNullOrEmpty() return when { cardWithHolder -> { card.holder } else -> "none" } }
  48. Smart-casts from variables class Card(val holder: String?) fun findHolder(card: Any):

    String { val cardWithHolder = card is Card && !card.holder.isNullOrEmpty() return when { cardWithHolder -> { card.holder } else -> "none" } }
  49. Smart-casts from variables class Card(val holder: String?) fun findHolder(card: Any):

    String { val cardWithHolder = card is Card && !card.holder.isNullOrEmpty() return when { cardWithHolder -> { card.holder } else -> "none" } } Any -> Card Smart-casted to Card
  50. Smart-casts from variables class Card(val holder: String?) fun findHolder(card: Any):

    String { val cardWithHolder = card is Card && !card.holder.isNullOrEmpty() return when { cardWithHolder -> { card.holder } else -> "none" } } Any -> Card String? -> String Smart-casted to String Smart-casted to Card
  51. What's next for Kotlin? Guards: pattern matching without binding Context

    parameters Name-based destructuring Union types for errors E ff ect system capabilities (Contracts)
  52. What's next for Kotlin? Guards: pattern matching without binding -

    2.1 Name-based destructuring - 2.2 Union types for errors - 2.x Context parameters - 2.2 E ff ect system capabilities (Contracts)
  53. when { order is YearlySubscription && order.amount > 100 ->

    applyDiscount(order) order is MonthlySubscription -> startSubscription(order) order is OneTimeOrder -> processOrder(order) } val order = getOrder()
  54. when { order is YearlySubscription && order.amount > 100 ->

    applyDiscount(order) order is MonthlySubscription -> startSubscription(order) order is OneTimeOrder -> processOrder(order) } val order = getOrder() Potentially a logical error Repetition is not nice
  55. when { order is YearlySubscription && order.amount > 100 ->

    applyDiscount(order) order is YearlySubscription -> processSubscription(order) order is MonthlySubscription -> startSubscription(order) order is OneTimeOrder -> processOrder(order) } val order = getOrder()
  56. when(order) { is YearlySubscription && order.amount > 100 -> applyDiscount(order)

    is YearlySubscription -> processSubscription(order) is MonthlySubscription -> startSubscription(order) is OneTimeOrder -> processOrder(order) } val order = getOrder()
  57. when(order) { is YearlySubscription && order.amount > 100 -> applyDiscount(order)

    is YearlySubscription -> processSubscription(order) is MonthlySubscription -> startSubscription(order) is OneTimeOrder -> processOrder(order) } val order = getOrder() Error: expecting ' -> ' &&
  58. when(order) { is YearlySubscription && order.amount > 100 -> applyDiscount(order)

    is YearlySubscription -> processSubscription(order) is MonthlySubscription -> startSubscription(order) is OneTimeOrder -> processOrder(order) } val order = getOrder() Guarded conditions: KEEP-371 if
  59. when(order) { is YearlySubscription if order.amount > 100 -> applyDiscount(order)

    is YearlySubscription -> processSubscription(order) is MonthlySubscription -> startSubscription(order) is OneTimeOrder -> processOrder(order) } val order = getOrder()
  60. when(order) { is YearlySubscription -> processSubscription(order) is YearlySubscription if order.amount

    > 100 -> applyDiscount(order) is MonthlySubscription -> startSubscription(order) is OneTimeOrder -> processOrder(order) } val order = getOrder() 'when' branch is never reachable
  61. when(order) { is YearlySubscription if order.amount > 100 -> applyDiscount(order)

    is YearlySubscription -> processSubscription(order) is MonthlySubscription -> startSubscription(order) is OneTimeOrder -> processOrder(order) } val order = getOrder()
  62. when(order) { is YearlySubscription if order.amount > 100 -> {

    val (id, name, amount) = order println("Order $id: $name $amount") } is YearlySubscription -> processSubscription(order) ... val order = getOrder() Destructuring
  63. when(order) { is YearlySubscription if order.amount > 100 -> {

    val (id, name, amount) = order println("Order $id: $name $amount") } is YearlySubscription -> processSubscription(order) ... val order = YearlySubscription("1", "Anton", 12.0, 2024 OCTOBER 9) Destructuring Order 1: Anton, 12.0
  64. when(order) { is YearlySubscription if order.amount > 100 -> {

    val (name, id, amount) = order println("Order $id: $name $amount") } is YearlySubscription -> processSubscription(order) ... val order = YearlySubscription("1", "Anton", 12.0, 2024 OCTOBER 9) Variable name 'id' matches the name of a di ff erent component Destructuring
  65. when(order) { is YearlySubscription if order.amount > 100 -> {

    val (name, id, amount) = order println("Order $id: $name $amount") } is YearlySubscription -> processSubscription(order) ... val order = YearlySubscription("1", "Anton", 12.0, 2024 OCTOBER 9) Order Anton: 1, 12.0 Destructuring
  66. when(order) { is YearlySubscription if order.amount > 100 -> {

    val (name, id, amount) = order println("Order $id: $name $amount") } is YearlySubscription -> processSubscription(order) ... val order = YearlySubscription("1", "Anton", 12.0, 2024 OCTOBER 9) Error in 2.x: 'name' doesn’t match the property 'customerName' Name-based destructuring
  67. /** * Returns the last element matching the given [predicate].

    */ public inline fun <T> Sequence<T>.last(predicate: (T) -> Boolean): T { var result: T? = null for (element in this) if (predicate(element)) result = element return result ? : throw NoSuchElementException("Not found") } Find last matching element in the sequence orders.last { it.amount > threshold }
  68. What if the predicate is '{ it == null }'

    /** * Returns the last element matching the given [predicate]. */ public inline fun <T> Sequence<T>.last(predicate: (T) -> Boolean): T { var last: T? = null var found = false for (element in this) { if (predicate(element)) { last = element found = true } } if (!found) throw NoSuchElementException("Not found") @Suppress("UNCHECKED_CAST") return last as T }
  69. private object NotFound fun <T> Sequence<T>.last(predicate: (T) -> Boolean): T

    { var result: Any? = NotFound for (element in this) if (predicate(element)) result = element if (result == = NotFound) throw NoSuchElementException("Not found") return result as T } Can we do better?
  70. private object NotFound fun <T> Sequence<T>.last(predicate: (T) -> Boolean): T

    { var result: Any? = NotFound for (element in this) if (predicate(element)) result = element if (result == = NotFound) throw NoSuchElementException("Not found") return result as T } Use of 'Any?' type Unchecked cast Can we do better?
  71. Union types for errors private error object NotFound fun <T>

    Sequence<T>.last(predicate: (T) -> Boolean): T { var result: T | NotFound = NotFound for (element in this) if (predicate(element)) result = element if (result is NotFound) throw NoSuchElementException("Not found") return result } Union types for errors Automatic smart-cast In research
  72. class Client(var name: String? = null, var birthday: LocalDate? =

    null) fun buildClient(init: Client.() - > Unit): Client { var client = Client() client.init() return client } Use case: type-safe builders (a.k.a DSLs)
  73. class Client(var name: String? = null, var birthday: LocalDate? =

    null) fun buildClient(init: Client.() - > Unit): Client { var client = Client() client.init() return client } Use case: type-safe builders DSL library
  74. class Client(var name: String? = null, var birthday: LocalDate? =

    null) fun buildClient(init: Client.() - > Unit): Client { var client = Client() client.init() return client } Use case: type-safe builders DSL library buildClient { name = "Bob" birthday = LocalDate.of(2000, 3, 10) } User code
  75. class Client(var name: String? = null, var birthday: LocalDate? =

    null) fun buildClient(init: Client.() - > Unit): Client { var client = Client() client.init() return client } Use case: type-safe builders DSL library buildClient { name = "Bob" birthday = LocalDate.of(2000, 3, 10) } User code Can we do better?
  76. class Client(var name: String? = null, var birthday: LocalDate? =

    null) fun buildClient(init: Client.() - > Unit): Client { var client = Client() client.init() return client } Use case: type-safe builders DSL library buildClient { name = "Bob" birthday = 10 March 2000 } User code infix fun Int.March(year: Int) = LocalDate.of(year, Month.MARCH, this) val dob = 10 March 2000
  77. class Client(var name: String? = null, var birthday: LocalDate? =

    null) fun buildClient(init: Client.() - > Unit): Client { var client = Client() client.init() return client } Use case: type-safe builders DSL library buildClient { name = "Bob" birthday = 10 March 2000 } User code infix fun Int.March(year: Int) = LocalDate.of(year, Month.MARCH, this) val dob = 10 March 2000 How can we restrict the scope?
  78. object ClientBuilderContext context(_: ClientBuilderContext) infix fun Int.March(year: Int) = LocalDate.of(year,

    Month.MARCH, this) DSL library buildClient { name = "Bob" birthday = 10 March 2000 } User code val dob = 10 March 2000 Context parameters (KEEP-367)
  79. DSL library buildClient { name = "Bob" birthday = 10

    March 2000 } User code val dob = 10 March 2000 fun buildClient(init: context(ClientBuilderContext) Client.() - > Unit): Client = with(ClientBuilderContext()) { //. .. } object ClientBuilderContext context(_: ClientBuilderContext) infix fun Int.March(year: Int) = LocalDate.of(year, Month.MARCH, this) Context parameters (KEEP-367)
  80. DSL library buildClient { name = "Bob" birthday = 10

    March 2000 } User code val dob = 10 March 2000 fun buildClient(init: context(ClientBuilderContext) Client.() - > Unit): Client = with(ClientBuilderContext()) { //. .. } object ClientBuilderContext context(_: ClientBuilderContext) infix fun Int.March(year: Int) = LocalDate.of(year, Month.MARCH, this) Context parameters (KEEP-367)
  81. DSL library buildClient { name = "Bob" birthday = 10

    March 2000 } User code val dob = 10 March 2000 fun buildClient(init: context(ClientBuilderContext) Client.() - > Unit): Client = with(ClientBuilderContext()) { //. .. } object ClientBuilderContext context(_: ClientBuilderContext) infix fun Int.March(year: Int) = LocalDate.of(year, Month.MARCH, this) The required context is missing Required context available in this block Context parameters (KEEP-367)
  82. buildClient { name = "Bob" // 'name' property stays uninitialized

    birthday = 10 March 2000 } Or... What if the user forgets to assign a property?
  83. E ff ect system capabilities (or Contracts?) buildClient { name

    = "Bob" birthday = 10 March 2000 } This feature doesn't exist
  84. E ff ect system capabilities buildClient { name = "Bob"

    birthday = 10 March 2000 } In research
  85. E ff ect system capabilities buildClient { name = "Bob"

    birthday = 10 March 2000 } fun buildClient(init: Client.() - > Unit): Client { contract { called(init@name, ONCE) called(init@birthday, ONCE) } / /... } In research
  86. E ff ect system capabilities buildClient { name = "Bob"

    birthday = 10 March 2000 } fun buildClient(init: Client.() - > Unit): Client { contract { called(init@name, ONCE) called(init@birthday, ONCE) } / /... } In research
  87. E ff ect system capabilities buildClient { name = "Bob"

    birthday = 10 March 2000 } fun buildClient(init: Client.() - > Unit): Client { contract { called(init@name, ONCE) called(init@birthday, ONCE) } / /... } In research Contract: "Ensure that the 'name' property is assigned once in the 'init' block"
  88. Summary Kotlin 2.0: new compiler & more smart-casts More features

    are coming for working with data Stronger abstractions and improvements in the type system