$30 off During Our Annual Pro Sale. View Details »

Idiomatic Kotlin - IntelliJ IDEA Conf 2022

Anton Arhipov
September 30, 2022

Idiomatic Kotlin - IntelliJ IDEA Conf 2022

Anton Arhipov

September 30, 2022
Tweet

More Decks by Anton Arhipov

Other Decks in Programming

Transcript

  1. Idiomatic Kotlin

  2. @antonarhipov

  3. None
  4. None
  5. None
  6. Idiomatic - using, containing, or denoting expressions that are natural

    to a native speaker
  7. Idiomatic - using, containing, or denoting expressions that are natural

    to a native speaker Commonly accepted style
  8. Idiomatic - using, containing, or denoting expressions that are natural

    to a native speaker Commonly accepted style E ff ective use of language features
  9. Top-Level (Extension) Functions

  10. fun main() { doSomething() } fun doSomething() { doMoreStuff( :

    : finishWork) } fun doMoreStuff(callback: () -> Unit) { callback() } fun finishWork() { TODO("Not implemented yet") }
  11. fun main() { doSomething() } fun doSomething() { doMoreStuff( :

    : finishWork) } fun doMoreStuff(callback: () -> Unit) { callback() } fun finishWork() { TODO("Not implemented yet") }
  12. fun main() { doSomething() } fun doSomething() { doMoreStuff( :

    : finishWork) } fun doMoreStuff(callback: () -> Unit) { callback() } fun finishWork() { TODO("Not implemented yet") }
  13. fun main() { doSomething() } fun doSomething() { doMoreStuff( :

    : finishWork) } fun doMoreStuff(callback: () -> Unit) { callback() } fun finishWork() { TODO("Not implemented yet") } Just functions, no classes!
  14. Don’t create classes just to hold functions!

  15. class StringUtils { companion object { fun isPhoneNumber(s: String) =

    s.length = = 7 & & s.all { it.isDigit() } } }
  16. class StringUtils { companion object { fun isPhoneNumber(s: String) =

    s.length = = 7 & & s.all { it.isDigit() } } } object StringUtils { fun isPhoneNumber(s: String) = s.length = = 7 & & s.all { it.isDigit() } }
  17. class StringUtils { companion object { fun isPhoneNumber(s: String) =

    s.length = = 7 & & s.all { it.isDigit() } } } object StringUtils { fun isPhoneNumber(s: String) = s.length = = 7 & & s.all { it.isDigit() } } fun isPhoneNumber(s: String) = s.length == 7 && s.all { it.isDigit() }
  18. class StringUtils { companion object { fun isPhoneNumber(s: String) =

    s.length = = 7 & & s.all { it.isDigit() } } } object StringUtils { fun isPhoneNumber(s: String) = s.length = = 7 & & s.all { it.isDigit() } } fun isPhoneNumber(s: String) = s.length == 7 && s.all { it.isDigit() } fun String.isPhoneNumber() = length == 7 && all { it.isDigit() }
  19. Extension or a member? https://kotlinlang.org/docs/coding-conventions.html#extension-functions Use extension functions liberally Restrict

    the visibility to minimize API pollution As necessary, use local extension functions, member extension functions, or top-level extension functions with private visibility
  20. +

  21. fun findMessageById(id: String) = db.query( "select * from messages where

    id = ?", RowMapper { rs, _ -> Message(rs.getString("id"), rs.getString("text")) }, id ) val db: JdbcTemplate = ...
  22. fun findMessageById(id: String) = db.query( "select * from messages where

    id = ?", RowMapper { rs, _ -> Message(rs.getString("id"), rs.getString("text")) }, id ) @Override public <T> List<T> query(String sql, RowMapper<T> rowMapper, @Nullable Object . .. args) throws DataAccessException { return result(query(sql, args, new RowMapperResultSetExtractor < > (rowMapper))); }
  23. fun findMessageById(id: String) = db.query( "select * from messages where

    id = ?", RowMapper { rs, _ -> Message(rs.getString("id"), rs.getString("text")) }, id )
  24. fun findMessageById(id: String) = db.query( "select * from messages where

    id = ?", RowMapper { rs, _ -> Message(rs.getString("id"), rs.getString("text")) }, id )
  25. fun findMessageById(id: String) = db.query( "select * from messages where

    id = ?", id, RowMapper { rs, _ -> Message(rs.getString("id"), rs.getString("text")) } ) vararg
  26. fun findMessageById(id: String) = db.query( "select * from messages where

    id = ?", id, { rs, _ -> Message(rs.getString("id"), rs.getString("text")) } ) SAM conversion
  27. fun findMessageById(id: String) = db.query( "select * from messages where

    id = ?", id) { rs, _ -> Message(rs.getString("id"), rs.getString("text")) } Trailing lambda parameter
  28. fun findMessageById(id: String) = db.query("select * from messages where id

    = ?", id ) { rs, _ -> Message(rs.getString("id"), rs.getString("text")) }
  29. fun findMessageById(id: String) = db.query("select * from messages where id

    = ?", id ) { rs, _ -> Message(rs.getString("id"), rs.getString("text")) } fun <T> JdbcOperations.query(sql: String, vararg args: Any, function: (ResultSet, Int) -> T): List<T> = query(sql, RowMapper { rs, i -> function(rs, i) }, *args) Extension function!
  30. Summary Functions, Extensions, Trailing lambda,Varargs, SAM conversion

  31. Scope functions

  32. None
  33. val dataSource = BasicDataSource() dataSource.driverClassName = "com.mysql.jdbc.Driver" dataSource.url = "jdbc:mysql://domain:3309/db"

    dataSource.username = "username" dataSource.password = "password" dataSource.maxTotal = 40 dataSource.maxIdle = 40 dataSource.minIdle = 4
  34. val dataSource = BasicDataSource() dataSource.driverClassName = "com.mysql.jdbc.Driver" dataSource.url = "jdbc:mysql://domain:3309/db"

    dataSource.username = "username" dataSource.password = "password" dataSource.maxTotal = 40 dataSource.maxIdle = 40 dataSource.minIdle = 4 val dataSource = BasicDataSource().apply { driverClassName = "com.mysql.jdbc.Driver" url = "jdbc:mysql://domain:3309/db" username = "username" password = "password" maxTotal = 40 maxIdle = 40 minIdle = 4 }
  35. val dataSource = BasicDataSource().apply { driverClassName = "com.mysql.jdbc.Driver" url =

    "jdbc:mysql://domain:3309/db" username = "username" password = "password" maxTotal = 40 maxIdle = 40 minIdle = 4 } public inline fun <T> T.apply(block: T.() -> Unit): T { block() return this }
  36. val dataSource = BasicDataSource().apply { driverClassName = "com.mysql.jdbc.Driver" url =

    "jdbc:mysql://domain:3309/db" username = "username" password = "password" maxTotal = 40 maxIdle = 40 minIdle = 4 } public inline fun <T> T.apply(block: T.() -> Unit): T { block() return this } Lambda with receiver
  37. ?.let val order = retrieveOrder() if (order != null){ processCustomer(order.customer)

    }
  38. val order = retrieveOrder() if (order != null){ processCustomer(order.customer) }

    retrieveOrder() ?. let { processCustomer(it.customer) } retrieveOrder() ?. customer ?. let { :: processCustomer } or ?.let
  39. None
  40. fun makeDir(path: String) = path.let { File(it) }.also { it.mkdirs()

    }
  41. fun makeDir(path: String) = path.let { File(it) }.also { it.mkdirs()

    } Don’t overuse the scope functions!
  42. fun makeDir(path: String) = path.let { File(it) }.also { it.mkdirs()

    } fun makeDir(path: String) : File { val file = File(path) file.mkdirs() return file } Don’t overuse the scope functions! This is simpler!
  43. fun makeDir(path: String) = path.let { File(it) }.also { it.mkdirs()

    } fun makeDir(path: String) : File { val file = File(path) file.mkdirs() return file } Don’t overuse the scope functions! This is simpler! fun makeDir(path: String) = File(path).also { it.mkdirs() } OK, this one is actually fi ne :)
  44. Default argument values and named parameters

  45. fun find(name: String){ find(name, true) } fun find(name: String, recursive:

    Boolean){ } Function overloading
  46. fun find(name: String){ find(name, true) } fun find(name: String, recursive:

    Boolean){ } fun find(name: String, recursive: Boolean = true){ } Default argument value Function overloading
  47. fun find(name: String){ find(name, true) } fun find(name: String, recursive:

    Boolean){ } fun find(name: String, recursive: Boolean = true){ } fun main() { find("myfile.txt") } Default argument value Function overloading
  48. class Figure( val width: Int = 1, val height: Int

    = 1, val depth: Int = 1, color: Color = Color.BLACK, description: String = "This is a 3d figure", ) Figure(Color.RED, "Red figure")
  49. class Figure( val width: Int = 1, val height: Int

    = 1, val depth: Int = 1, color: Color = Color.BLACK, description: String = "This is a 3d figure", ) Figure(Color.RED, "Red figure") Compilation error
  50. class Figure( val width: Int = 1, val height: Int

    = 1, val depth: Int = 1, color: Color = Color.BLACK, description: String = "This is a 3d figure", ) Figure(color = Color.RED, description = "Red figure")
  51. Default argument values diminish the need for overloading in most

    cases.
  52. Default argument values diminish the need for overloading in most

    cases. Named parameters is a necessary tool for working with default argument values
  53. Expressions if when try

  54. fun adjustSpeed(weather: Weather): Drive { val result: Drive if (weather

    is Rainy) { result = Safe() } else { result = Calm() } return result }
  55. fun adjustSpeed(weather: Weather): Drive { val result: Drive if (weather

    is Rainy) { result = Safe() } else { result = Calm() } return result }
  56. fun adjustSpeed(weather: Weather): Drive { val result: Drive = if

    (weather is Rainy) { Safe() } else { Calm() } return result }
  57. fun adjustSpeed(weather: Weather): Drive { val result: Drive = if

    (weather is Rainy) { Safe() } else { Calm() } return result }
  58. fun adjustSpeed(weather: Weather): Drive { return if (weather is Rainy)

    { Safe() } else { Calm() } }
  59. fun adjustSpeed(weather: Weather): Drive { return if (weather is Rainy)

    { Safe() } else { Calm() } }
  60. fun adjustSpeed(weather: Weather): Drive = if (weather is Rainy) {

    Safe() } else { Calm() }
  61. fun adjustSpeed(weather: Weather): Drive = if (weather is Rainy) {

    Safe() } else { Calm() }
  62. fun adjustSpeed(weather: Weather) = if (weather is Rainy) { Safe()

    } else { Calm() }
  63. fun adjustSpeed(weather: Weather) = if (weather is Rainy) { Safe()

    } else { Calm() }
  64. fun adjustSpeed(weather: Weather) = if (weather is Rainy) Safe() else

    Calm() Is it concise? Sure!
  65. fun adjustSpeed(weather: Weather) = if (weather is Rainy) Safe() else

    Calm() Is it readable? It depends!
  66. fun adjustSpeed(weather: Weather) = if (weather is Rainy) Safe() else

    Calm() What does the function return?
  67. fun adjustSpeed(weather: Weather): Drive = ... fun adjustSpeed(weather: Weather) =

    ... For public API, keep the return type in the signature For private API it is generally OK to use type inference
  68. fun adjustSpeed(weather: Weather) = if (weather is Rainy) Safe() else

    Calm()
  69. abstract class Weather class Sunny : Weather() class Rainy :

    Weather() fun adjustSpeed(weather: Weather) = when (weather) { is Rainy -> Safe() else -> Calm() }
  70. sealed class Weather class Sunny : Weather() class Rainy :

    Weather() fun adjustSpeed(weather: Weather) = when (weather) { is Rainy -> Safe() / / else -> Calm() }
  71. sealed class Weather class Sunny : Weather() class Rainy :

    Weather() fun adjustSpeed(weather: Weather) = when (weather) { is Rainy -> Safe() / / else -> Calm() }
  72. sealed class Weather class Sunny : Weather() class Rainy :

    Weather() fun adjustSpeed(weather: Weather) = when (weather) { is Rainy -> Safe() is Sunny -> TODO() }
  73. sealed class Weather class Sunny : Weather() class Rainy :

    Weather() fun adjustSpeed(weather: Weather) = when (weather) { is Rainy -> Safe() is Sunny -> TODO() } Use expressions! Use when as expression body Use sealed classes with when
  74. Null-safety

  75. class Nullable { fun someFunction(){} } fun createNullable(): Nullable? =

    null fun main() { val n: Nullable? = createNullable() n.someFunction() }
  76. class Nullable { fun someFunction(){} } fun createNullable(): Nullable? =

    null fun main() { val n: Nullable? = createNullable() n.someFunction() }
  77. Consider using null-safe call val order = retrieveOrder() if (order

    == null || order.customer = = null || order.customer.address == null){ throw IllegalArgumentException("Invalid Order") } val city = order.customer.address.city
  78. Consider using null-safe call val order = retrieveOrder() val city

    = order ?. customer ? . address ?. city
  79. val order = retrieveOrder() val city = order ?. customer

    ? . address ?. city ?: throw IllegalArgumentException("Invalid Order") Consider using null-safe call
  80. Avoid not-null assertions ! ! val order = retrieveOrder() val

    city = order !! .customer !! .address !! .city “You may notice that the double exclamation mark looks a bit rude: it’s almost like you’re yelling at the compiler. This is intentional.” - Kotlin in Action
  81. Avoid not-null assertions ! ! class MyTest { class State(val

    data: String) private var state: State? = null @BeforeEach fun setup() { state = State("abc") } @Test fun foo() { assertEquals("abc", state !! .data) } }
  82. Avoid not-null assertions ! ! class MyTest { class State(val

    data: String) private var state: State? = null @BeforeEach fun setup() { state = State("abc") } @Test fun foo() { assertEquals("abc", state !! .data) } } class MyTest { class State(val data: String) private lateinit var state: State @BeforeEach fun setup() { state = State("abc") } @Test fun foo() { assertEquals("abc", state.data) } } - use lateinit
  83. None
  84. Use elvis operator class Person(val name: String?, val age: Int?)

    val p = retrievePerson() ?: Person()
  85. Use elvis operator as return and throw class Person(val name:

    String?, val age: Int?) fun processPerson(person: Person) { val name = person.name if (name = = null) throw IllegalArgumentException("Named required") val age = person.age if (age == null) return println("$name: $age") }
  86. Use elvis operator as return and throw class Person(val name:

    String?, val age: Int?) fun processPerson(person: Person) { val name = person.name if (name = = null) throw IllegalArgumentException("Named required") val age = person.age if (age == null) return println("$name: $age") }
  87. Use elvis operator as return and throw class Person(val name:

    String?, val age: Int?) fun processPerson(person: Person) { val name = person.name if (name = = null) throw IllegalArgumentException("Named required") val age = person.age if (age == null) return println("$name: $age") }
  88. Use elvis operator as return and throw class Person(val name:

    String?, val age: Int?) fun processPerson(person: Person) { val name = person.name ? : throw IllegalArgumentException("Named required") val age = person.age ?: return println("$name: $age") }
  89. Consider using safe cast for type checking override fun equals(other:

    Any?) : Boolean { val command = other as Command return command.id == id }
  90. Consider using safe cast for type checking override fun equals(other:

    Any?) : Boolean { val command = other as Command return command.id == id } override fun equals(other: Any?) : Boolean { return (other as? Command) ?. id == id }
  91. Ranges

  92. Use range checks instead of comparison pairs fun isLatinUppercase(c: Char)

    = c > = 'A' && c < = 'Z'
  93. fun isLatinUppercase(c: Char) = c > = 'A' && c

    < = 'Z' Use range checks instead of comparison pairs
  94. Use range checks instead of comparison pairs fun isLatinUppercase(c: Char)

    = c in 'A' . . 'Z'
  95. Use range checks instead of comparison pairs fun isLatinUppercase(c: Char)

    = c in 'A' . . 'Z'
  96. class Version(val major: Int, val minor: Int): Comparable<Version> { override

    fun compareTo(other: Version): Int { if (this.major != other.major) { return this.major - other.major } return this.minor - other.minor } } fun main() { val versionRange = Version(1, 11) . . Version(1, 30) println(Version(0, 9) in versionRange) println(Version(1, 20) in versionRange) } Comparable range
  97. class Version(val major: Int, val minor: Int): Comparable<Version> { override

    fun compareTo(other: Version): Int { if (this.major != other.major) { return this.major - other.major } return this.minor - other.minor } } fun main() { val versionRange = Version(1, 11) . . Version(1, 30) println(Version(0, 9) in versionRange) println(Version(1, 20) in versionRange) } Comparable range public operator fun <T : Comparable<T > > T.rangeTo(that: T): ClosedRange<T> = ComparableRange(this, that)
  98. class Version(val major: Int, val minor: Int): Comparable<Version> { override

    fun compareTo(other: Version): Int { if (this.major != other.major) { return this.major - other.major } return this.minor - other.minor } } fun main() { val versionRange = Version(1, 11) . . Version(1, 30) println(Version(0, 9) in versionRange) println(Version(1, 20) in versionRange) } Comparable range public operator fun <T : Comparable<T > > T.rangeTo(that: T): ClosedRange<T> = ComparableRange(this, that)
  99. Ranges in loops fun main(args: Array<String>) { for (i in

    0 .. args.size - 1) { println("$i: ${args[i]}") } }
  100. Ranges in loops fun main(args: Array<String>) { for (i in

    0 .. args.size - 1) { println("$i: ${args[i]}") } }
  101. Ranges in loops fun main(args: Array<String>) { for (i in

    0 .. args.size - 1) { println("$i: ${args[i]}") } } for (i in 0 until args.size) { println("$i: ${args[i]}") }
  102. Ranges in loops fun main(args: Array<String>) { for (i in

    0 .. args.size - 1) { println("$i: ${args[i]}") } } for (i in 0 until args.size) { println("$i: ${args[i]}") } for (i in args.indices) { println("$i: ${args[i]}") }
  103. Ranges in loops fun main(args: Array<String>) { for (i in

    0 .. args.size - 1) { println("$i: ${args[i]}") } } for (i in 0 until args.size) { println("$i: ${args[i]}") } for (i in args.indices) { println("$i: ${args[i]}") } for ((i, arg) in args.withIndex()) { println("$i: $arg") }
  104. None
  105. None
  106. None
  107. Standard library

  108. abcd bcde cdef defg xya xyb xyc Group 1: Group

    2: Count the total of characters that are present on each line in every group of strings
  109. abcd bcde cdef defg xya xyb xyc Group 1: Group

    2: Count the total of characters that are present on each line in every group of strings
  110. abcd bcde cdef defg xya xyb xyc Group 1: Group

    2: {d} {xy} Count the total of characters that are present on each line in every group of strings
  111. abcd bcde cdef defg xya xyb xyc Group 1: Group

    2: {d} {xy} count = 1 count = 2 Count the total of characters that are present on each line in every group of strings
  112. abcd bcde cdef defg xya xyb xyc Group 1: Group

    2: {d} {xy} count = 1 count = 2 Total = 3 Count the total of characters that are present on each line in every group of strings
  113. val input = """ abcd bcde cdef defg xya xyb

    xyc """.trimIndent()
  114. val input = """ abcd bcde cdef defg xya xyb

    xyc """.trimIndent() val groups: List<String> = input.split("\n\n")
  115. val input = """ abcd bcde cdef defg xya xyb

    xyc """.trimIndent() val groups: List<String> = input.split("\n\n")
  116. val input = """ abcd bcde cdef defg xya xyb

    xyc """.trimIndent() val groups: List<String> = input.split("\n\n") var total = 0 for (group in groups) { val listOfSets: List<Set<Char >> = group.split("\n").map(String :: toSet) var result = listOfSets.first() for (set in listOfSets) { result = result intersect set } total += result.count() }
  117. val input = """ abcd bcde cdef defg xya xyb

    xyc """.trimIndent() val groups: List<String> = input.split("\n\n") var total = 0 for (group in groups) { val listOfSets: List<Set<Char >> = group.split("\n").map(String :: toSet) var result = listOfSets.first() for (set in listOfSets) { result = result intersect set } total += result.count() }
  118. val input = """ abcd bcde cdef defg xya xyb

    xyc """.trimIndent() val groups: List<String> = input.split("\n\n") var total = 0 for (group in groups) { val listOfSets: List<Set<Char >> = group.split("\n").map(String :: toSet) var result = listOfSets.first() for (set in listOfSets) { result = result intersect set } total += result.count() }
  119. val input = """ abcd bcde cdef defg xya xyb

    xyc """.trimIndent() val groups: List<String> = input.split("\n\n") var total = 0 for (group in groups) { val listOfSets: List<Set<Char >> = group.split("\n").map(String :: toSet) var result = listOfSets.first() for (set in listOfSets) { result = result intersect set } total += result.count() }
  120. val input = """ abcd bcde cdef defg xya xyb

    xyc """.trimIndent() val groups: List<String> = input.split("\n\n") var total = 0 for (group in groups) { val listOfSets: List<Set<Char >> = group.split("\n").map(String :: toSet) var result = listOfSets.first() for (set in listOfSets) { result = result intersect set } total += result.count() } Transforming data
  121. val input = """ abcd bcde cdef defg xya xyb

    xyc """.trimIndent() val groups: List<String> = input.split("\n\n") var total = 0 for (group in groups) { val listOfSets: List<Set<Char >> = group.split("\n").map(String :: toSet) var result = listOfSets.first() for (set in listOfSets) { result = result intersect set } total += result.count() } Calculating the result Transforming data
  122. val groups: List<String> = input.split("\n\n")

  123. val groups: List<String> = input.split("\n\n") // List<List<Set<Char >>> val step1

    = groups.map { it.split("\n").map(String :: toSet) } Transforming data
  124. val groups: List<String> = input.split("\n\n") // List<List<Set<Char >>> val step1

    = groups.map { it.split("\n").map(String :: toSet) } Transforming data
  125. val groups: List<String> = input.split("\n\n") // List<List<Set<Char >>> val step1

    = groups.map { it.split("\n").map(String :: toSet) } Transforming data
  126. val groups: List<String> = input.split("\n\n") // List<List<Set<Char >>> val step1

    = groups.map { it.split("\n").map(String :: toSet) } Transforming data
  127. val groups: List<String> = input.split("\n\n") // List<List<Set<Char >>> val step1

    = groups.map { it.split("\n").map(String :: toSet) } Transforming data
  128. val groups: List<String> = input.split("\n\n") // List<List<Set<Char >>> val step1

    = groups.map { it.split("\n").map(String :: toSet) } val step2 = step1.sumOf { it.reduce { a, b - > a intersect b }.count() } Transforming data Calculating the result
  129. val groups: List<String> = input.split("\n\n") // List<List<Set<Char >>> val step1

    = groups.map { it.split("\n").map(String :: toSet) } val step2 = step1.sumOf { it.reduce { a, b - > a intersect b }.count() } Calculating the result Transforming data
  130. val groups: List<String> = input.split("\n\n") // List<List<Set<Char >>> val step1

    = groups.map { it.split("\n").map(String :: toSet) } val step2 = step1.sumOf { it.reduce { a, b - > a intersect b }.count() } Calculating the result Transforming data
  131. val groups: List<String> = input.split("\n\n") // List<List<Set<Char >>> val step1

    = groups.map { it.split("\n").map(String :: toSet) } val step2 = step1.sumOf { it.reduce { a, b - > a intersect b }.count() } Calculating the result Transforming data
  132. val groups: List<String> = input.split("\n\n") // List<List<Set<Char >>> val step1

    = groups.map { it.split("\n").map(String :: toSet) } val step2 = step1.sumOf { it.reduce { a, b - > a intersect b }.count() } Calculating the result Transforming data
  133. val groups: List<String> = input.split("\n\n") // List<List<Set<Char >>> val step1

    = groups.map { it.split("\n").map(String :: toSet) } val step2 = step1.sumOf { it.reduce { a, b - > a intersect b }.count() } groups.map { group -> group.split(nl).map(String :: toSet) }.sumOf { answerSets -> answerSets.reduce { a, b -> a intersect b }.count() } Calculating the result Transforming data
  134. None
  135. None
  136. kotlin { twitter = "@kotlin" youtube = "youtube.com/kotlin" slack =

    "slack.kotl.in" } me { name = "Anton Arhipov" twitter = "@antonarhipov" slides = "speakerdeck.com/antonarhipov" }