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

Devoxx France 2024. Kotlin - the new and notewo...

Devoxx France 2024. Kotlin - the new and noteworthy

Anton Arhipov

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

    But now Java has records . public record Person( String name, int age ) {}
  5. 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
  6. 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!
  7. 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!!
  8. 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!!!
  9. 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
  10. 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
  11. 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
  12. fun main() { val event: String? = getEventName() println("Hello, ${event.uppercase()}!")

    println("Hello, ${event.randomCase(offset = 0.25)}!") } private fun getEventName(): String? = "Devoxx" Nullable types
  13. 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
  14. "Type-safe builders" a.k.a DSLs val client = createClient { firstName

    = "Anton" lastName = "Arhipov" twitter { handle = "@antonarhipov" } company { name = "JetBrains" city = "Tallinn" } dob = 24 March 2000 } println("Created client is: ${client.toS}")
  15. 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
  16. Features added after Kotlin 1.0 : - Multiplatform projects -

    Coroutines - Inline / Value classes - Trailing comma - fun inter f aces - .
  17. 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
  18. 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
  19. 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
  20. 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
  21. 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
  22. Kotlin 2.0 ( 2024 ) 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
  23. Kotlin 2.0 ( 2024 ) 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
  24. 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)
  25. 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)
  26. fun mutate(ml: MutableList<Long>) { ml[0] = ml[0] + 1 }

    Combination of Long and Integer Literal Types Frontend Intermediate Representation (FIR)
  27. 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)
  28. 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)
  29. 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)
  30. 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)
  31. 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)
  32. 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) - .
  33. class Cat { fun purr() { println("Purr purr") } }

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

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

    fun petAnimal(animal: Any) { val isCat = animal is Cat if (isCat) { animal.purr() } } Smar t -casts from variables
  36. class Cat { fun purr() { println("Purr purr") } }

    fun petAnimal(animal: Any) { val isCat = animal is Cat if (isCat) { animal.purr() } } Smar t -casts from variables Kotlin 2.0 : synthetic data flow variables propagate information about smar t -casts
  37. Smar t -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" } }
  38. Smar t -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" } }
  39. Smar t -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 Smar t -casted to Card
  40. Smar t -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 Smar t -casted to String Smar t -casted to Card
  41. Smar t -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 Smar t -casted to String Smar t -casted to Card String? -> String
  42. Smar t -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. Smar t -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" } } String
  44. What's next for Kotlin? More features are coming for working

    with data Stronger abstractions and improvements in the type system
  45. What's next for Kotlin? Guards: pattern matching without binding Context

    parameters 2.2 CheckReturnValue 2.2 Name-based destructuring 2.4 Rich errors 2.4 Beta Stable 2.2
  46. when { order is YearlySubscription && order.amount > 100 ->

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

    is YearlySubscription -> processSubscription(order) is MonthlySubscription -> startSubscription(order) is OneTimeOrder -> processOrder(order) } val order = getOrder()
  50. 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 ' -> ' &&
  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() Guarded conditions: KEEP - 371 if
  52. 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()
  53. 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
  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() 2.1
  55. 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 2.2 Stabe
  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()
  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: 'name' doesn’t match the proper t y 'customerName' Name-based destructuring
  62. 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) Name-based destructuring
  63. 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) Name-based destructuring Existing syntax
  64. when(order) { is YearlySubscription if order.amount > 100 -> {

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

    (val name, val id, val amount) = order println("Order $id: $name $amount") } is YearlySubscription -> processSubscription(order) ... val order = YearlySubscription("1", "Anton", 12.0, 2024 OCTOBER 9) Name-based destructuring sealed interface Order data class YearlySubscription(val productName: String, ... ) : Order Does not rely on data classes
  66. when(order) { is YearlySubscription if order.amount > 100 -> {

    (val name, val id, val amount) = order println("Order $id: $name $amount") } is YearlySubscription -> processSubscription(order) ... val order = YearlySubscription("1", "Anton", 12.0, 2024 OCTOBER 9) Name-based destructuring sealed interface Order data class YearlySubscription(val productName: String, ... ) : Order Experimental in 2.4 Does not rely on data classes
  67. What's next for Kotlin? Guards: pattern matching without binding Context

    parameters 2.2 CheckReturnValue 2.2 Name-based destructuring 2.4 Rich errors 2.4 Beta Stable 2.2
  68. 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)
  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) Functional literal with receiver
  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. 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 ) Beta in 2.2
  80. What's next for Kotlin? Guards: pattern matching without binding Context

    parameters 2.2 CheckReturnValue 2.2 Name-based destructuring 2.4 Rich errors 2.4 Beta Stable 2.2
  81. Errors are a way to express that a method can

    fail using type system capabilities error object NotFound TL;DR: Errors != Exceptions
  82. Errors are a way to express that a method can

    fail using type system capabilities error object NotFound TL;DR: Errors != Exceptions Experimental in 2.4
  83. What about exceptions? • Kotlin don’t have checked exceptions for

    a purpose • Primary use exceptions should be for unrecoverable cases
  84. What about exceptions? • Kotlin don’t have checked exceptions for

    a purpose • Primary use exceptions should be for unrecoverable cases From exceptions to errors:
  85. What about exceptions? • Kotlin don’t have checked exceptions for

    a purpose • Primary use exceptions should be for unrecoverable cases From exceptions to errors:
  86. What's next for Kotlin? Guards: pattern matching without binding Context

    parameters 2.2 CheckReturnValue 2.2 Name-based destructuring 2.4 Rich errors 2.4 Beta Stable 2.2
  87. They do not use return value of the call. What

    do those examples have in common?
  88. They do not use return value of the call. What

    do those examples have in common? Can we prevent this?
  89. They do not use return value of the call. What

    do those examples have in common? Can we prevent this? Let's repor t every unused value!
  90. They do not use return value of the call. What

    do those examples have in common? Can we prevent this? Let's repor t every unused value! Not always true, unfor t unately:
  91. They do not use return value of the call. What

    do those examples have in common? Can we prevent this? Let's repor t every unused value! Not always true, unfor t unately: There are results which are supposed to be used and there are results which are auxiliary.
  92. CheckReturnValue as future default In Kotlin 2.2 : Kotlin stdlib

    and kotlinx libraries are marked with @MustUseReturnValue Opt-in to enable the checker
  93. CheckReturnValue as future default In Kotlin 2.2 : Kotlin stdlib

    and kotlinx libraries are marked with @MustUseReturnValue Opt-in to enable the checker Next steps: More libraries Application code by default
  94. What's next for Kotlin? Guards: pattern matching without binding Context

    parameters 2.2 CheckReturnValue 2.2 Name-based destructuring 2.4 Rich errors 2.4 Beta Stable 2.2
  95. 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