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

VirtualJUG: Kotlin 2.0 and beyond

VirtualJUG: 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

January 29, 2025
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 May 21, 2024 - Kotlin 2.0
  5. July 20, 2011 - Kotlin announced at JVMLS February 15,

    2016 - Kotlin 1.0 May 21, 2024 - Kotlin 2.0 1.1, 1.2, 1.3, 1.4, 1.5, 1.6.x, 1.7.x, 1.8.x, 1.9.x
  6. July 20, 2011 - Kotlin announced at JVMLS February 15,

    2016 - Kotlin 1.0 2019 May 21, 2024 - Kotlin 2.0 (with K2 ) May 2025 ( ?) - Kotlin 2.2 Work star t ed on the new compiler front-end (K2 )
  7. July 20, 2011 - Kotlin announced at JVMLS February 15,

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

    2016 - Kotlin 1.0 May 21, 2024 - Kotlin 2.0 November 27, 2024 - Kotlin 2.1 May 2025 ( ?) - Kotlin 2.2
  9. data class Person( val name: String, val age: Int )

    It used to be very easy to create the wow effect 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 + '}'; } }
  10. data class Person( val name: String, val age: Int )

    But now Java has records . public record Person( String name, int age ) {}
  11. 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
  12. 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!
  13. 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!!
  14. 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!!!
  15. 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
  16. 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
  17. 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
  18. fun main() { val event: String? = getEventName() println("Hello, ${event.uppercase()}!")

    println("Hello, ${event.randomCase(offset = 0.25)}!") } private fun getEventName(): String? = "Devoxx" Nullable types
  19. 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
  20. Kotlin still has a lot to offer: These features combined

    make a huge difference in how we reason about the code and structure Kotlin programs
  21. Features added after Kotlin 1.0 : - Multiplatform projects -

    Coroutines - Inline / Value classes - Trailing comma - fun inter f aces - .
  22. Features added after Kotlin 1.0 : - Multiplatform projects -

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

    Coroutines - Inline / Value classes - Trailing comma - fun inter f aces - Type aliases - Sealed classes & inter f aces - 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 - Definitely non-nullable types - Instantiation of annotation classes - Suppor t for JSpecify - suspend functions as super t ypes - Secondary constructors for inline value classes A LOT
  24. 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 per f ormance
  25. 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 per f ormance
  26. 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 per f ormance
  27. Kotlin 2.0 More than 80 features in the different subsystems

    Around 25 and small improvements within the language Main focus is on correctness and per f ormance
  28. 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)
  29. 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)
  30. fun mutate(ml: MutableList<Long>) { ml[0] = ml[0] + 1 }

    Combination of Long and Integer Literal Types Frontend Intermediate Representation (FIR)
  31. 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)
  32. 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)
  33. 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)
  34. 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)
  35. 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)
  36. New control flow engine read: more smar t -casts! -

    KT-7186 Smar t cast for captured variables inside changing closures of inline functions - KT-4113 Smar t casts for proper t ies to not-null functional types at invoke calls - KT-25747 DFA variables: propagate smar t cast results from local variables - KT-1982 Smar t cast to a common super t ype of subject types after || (OR operator) - .
  37. class Cat { fun purr() { println("Purr purr") } }

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

    fun petAnimal(animal: Any) { if (animal is Cat) { animal.purr() } } Smart-casts
  39. 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
  40. 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
  41. 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
  42. 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" } }
  43. 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" } }
  44. 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
  45. 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
  46. What's next for Kotlin? Guards: pattern matching without binding Name-based

    destructuring Union types for errors Context parameters Effect system capabilities (Contracts) ... and more
  47. 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 Effect system capabilities (Contracts) ... and more
  48. when { order is YearlySubscription && order.amount > 100 ->

    applyDiscount(order) order is MonthlySubscription -> startSubscription(order) order is OneTimeOrder -> processOrder(order) } val order = getOrder()
  49. 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
  50. 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()
  51. when(order) { is YearlySubscription && order.amount > 100 -> applyDiscount(order)

    is YearlySubscription -> processSubscription(order) is MonthlySubscription -> startSubscription(order) is OneTimeOrder -> processOrder(order) } val order = getOrder()
  52. 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 ' -> ' &&
  53. 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
  54. 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()
  55. 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
  56. 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() 2.1
  57. 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
  58. 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
  59. 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 different component Destructuring
  60. 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
  61. 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
  62. 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 Effect system capabilities (Contracts) ... and more
  63. /** * 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 }
  64. 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 }
  65. 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?
  66. 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?
  67. 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 smar t -cast In research
  68. 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 Effect system capabilities (Contracts) ... and more
  69. 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)
  70. 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
  71. 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
  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 DSL library buildClient { name = "Bob" birthday = LocalDate.of(2000, 3, 10) } User code Can we do better?
  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 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
  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 = 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?
  75. 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 )
  76. 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 )
  77. 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 )
  78. 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 )
  79. 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 Effect system capabilities (Contracts) ... and more
  80. buildClient { name = "Bob" // 'name' property stays uninitialized

    birthday = 10 March 2000 } Or ... What if the user forgets to assign a proper t y?
  81. Effect system capabilities (or Contracts?) buildClient { name = "Bob"

    birthday = 10 March 2000 } This feature doesn't exist
  82. Effect 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
  83. Effect 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
  84. Effect 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' proper t y is assigned once in the 'init' block"
  85. Summary Kotlin 2.0 : new compiler & more smar t

    -casts More features are coming for working with data Stronger abstractions and improvements in the type system