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

Idiomatic Kotlin, v2023.05

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.

Idiomatic Kotlin, v2023.05

Avatar for Anton Arhipov

Anton Arhipov

May 09, 2023
Tweet

More Decks by Anton Arhipov

Other Decks in Programming

Transcript

  1. Top-Level Functions & Extensions Scope Functions Default argument values and

    named parameters Expressions Null-safety Type-safe builders, a.k.a DSL
  2. fun main() { doSomething() } fun doSomething() { doMoreStuff( :

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

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

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

    : finishWork) } fun doMoreStuff(callback: () -> Unit) { callback() } fun finishWork() { TODO("Not implemented yet") } Just functions, no classes!
  6. fun stopProducer() { if (this : : producer.isInitialized) { runBlocking

    { runCatching { producer.close() }.onFailure { println("failed to close queue producer: $it") } } } } fun stopBroker() { if (this : : broker.isInitialized) { runBlocking { runCatching { broker.close() }.onFailure { println("failed to close queue producer: $it") } } } } fun stopService() { if (this : : service.isInitialized) { runBlocking { runCatching { service.close() }.onFailure { println("failed to close queue producer: $it") } } } }
  7. fun stopProducer() { if (this : : producer.isInitialized) { runBlocking

    { runCatching { producer.close() }.onFailure { println("failed to close queue producer: $it") } } } } fun stopBroker() { if (this : : broker.isInitialized) { runBlocking { runCatching { broker.close() }.onFailure { println("failed to close queue producer: $it") } } } } fun stopService() { if (this : : service.isInitialized) { runBlocking { runCatching { service.close() }.onFailure { println("failed to close queue producer: $it") } } } } Duplicates
  8. fun stopProducer() { if (this : : producer.isInitialized) { runBlocking

    { runCatching { producer.close() }.onFailure { println("failed to close queue producer: $it") } } } } fun stopBroker() { if (this : : broker.isInitialized) { runBlocking { runCatching { broker.close() }.onFailure { println("failed to close queue producer: $it") } } } } fun stopService() { if (this : : service.isInitialized) { runBlocking { runCatching { service.close() }.onFailure { println("failed to close queue producer: $it") } } } } Di ff erent objects without common interface
  9. fun stopResource(predicate: () - > Boolean, close: suspend () ->

    Unit) { if (predicate()) { runBlocking { runCatching { close() }.onFailure { println("failed to close the resource: $it") } } } }
  10. fun stopResource(predicate: () - > Boolean, close: suspend () ->

    Unit) { if (predicate()) { runBlocking { runCatching { close() }.onFailure { println("failed to close the resource: $it") } } } }
  11. fun stopResource(predicate: () - > Boolean, close: suspend () ->

    Unit) { if (predicate()) { runBlocking { runCatching { close() }.onFailure { println("failed to close the resource: $it") } } } }
  12. class StringUtils { companion object { fun isPhoneNumber(s: String) =

    s.length = = 7 & & s.all { it.isDigit() } } }
  13. 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() } }
  14. 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() }
  15. 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() }
  16. 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
  17. +

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

    id = ?", RowMapper { rs, _ -> Message(rs.getString("id"), rs.getString("text")) }, id ) val db: JdbcTemplate = ...
  19. 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))); }
  20. fun findMessageById(id: String) = db.query( "select * from messages where

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

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

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

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

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

    = ?", id ) { rs, _ -> Message(rs.getString("id"), rs.getString("text")) }
  26. 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!
  27. 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
  28. 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 }
  29. 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 }
  30. 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
  31. ? . let val order = retrieveOrder() if (order !=

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

    retrieveOrder() ?. let { processCustomer(it.customer) } retrieveOrder() ?. customer ?. let( :: processCustomer) or ? . let
  33. let() as a helper for a complex condition if (some.complex.expression.let

    { it is Type && it.has.some.property }) { . .. }
  34. let() as a helper for a complex condition if (some.complex.expression.let

    { it is Type && it.has.some.property }) { . .. }
  35. let() as a helper for a complex condition if (some.complex.expression.let

    { it is Type && it.has.some.property }) { . .. }
  36. if (retrieveOrder().let { it is Subscription && it.customer.name == "Anton"})

    { . .. } let() as a helper for a complex condition if (some.complex.expression.let { it is Type && it.has.some.property }) { . .. }
  37. fun makeDir(path: String) = path.let { File(it) }.also { it.mkdirs()

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

    } fun makeDir(path: String) : File { val file = File(path) file.mkdirs() return file } fun makeDir(path: String) = File(path).also { it.mkdirs() } OK, this one is actually fi ne :) This is simpler! Don’t overuse the scope functions!
  39. 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
  40. 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
  41. 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")
  42. 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
  43. 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")
  44. Named parameters is a necessary tool for working with default

    argument values Default argument values diminish the need for overloading in most cases
  45. fun adjustSpeed(weather: Weather): Drive { val result: Drive if (weather

    is Rainy) { result = Safe() } else { result = Calm() } return result } Let’s transform this code using Alt+Enter
  46. fun adjustSpeed(weather: Weather): Drive { val result: Drive if (weather

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

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

    (weather is Rainy) { Safe() } else { Calm() } return result }
  49. 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
  50. abstract class Weather class Sunny : Weather() class Rainy :

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

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

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

    Weather() fun adjustSpeed(weather: Weather) = when (weather) { is Rainy -> Safe() is Sunny -> TODO() }
  54. 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 expression
  55. val condition: Boolean = expression() when(condition) { true -> doThis()

    false -> doThat() } if (condition) { doThis() } else { doThat() } if when
  56. Use try as expression body fun tryParse(number: String) : Int?

    { try { return Integer.parseInt(number) } catch (e: NumberFormatException) { return null } }
  57. fun tryParse(number: String) = try { Integer.parseInt(number) } catch (e:

    NumberFormatException) { null } Use try as expression body
  58. class Nullable { fun someFunction(){} } fun createNullable(): Nullable? =

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

    null fun main() { val n: Nullable? = createNullable() n.someFunction() }
  60. 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
  61. val order = retrieveOrder() val city = order ?. customer

    ? . address ?. city ?: throw IllegalArgumentException("Invalid Order") Consider using null-safe call
  62. 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
  63. 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) } }
  64. 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
  65. 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") }
  66. 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") }
  67. 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") }
  68. 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") }
  69. Consider using safe cast for type checking override fun equals(other:

    Any?) : Boolean { val command = other as Command return command.id == id }
  70. 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 }
  71. buildString //Java String name = "Joe"; StringBuilder sb = new

    StringBuilder(); for (int i = 0; i < 5; i++) { sb.append("Hello, "); sb.append(name); sb.append("!\n"); } System.out.println(sb); //Kotlin val name = "Joe" val s = buildString { repeat(5) { append("Hello, ") append(name) appendLine("!") } } println(s)
  72. Ktor fun main() { embeddedServer(Netty, port = 8080, host =

    "0.0.0.0") { routing { get("/html-dsl") { call.respondHtml { body { h1 { +"HTML" } ul { for (n in 1 .. 10) { li { +"$n" } } } } } } } }.start(wait = true) }
  73. Ktor fun main() { embeddedServer(Netty, port = 8080, host =

    "0.0.0.0") { routing { get("/html-dsl") { call.respondHtml { body { h1 { +"HTML" } ul { for (n in 1 .. 10) { li { +"$n" } } } } } } } }.start(wait = true) } Ktor’s routing
  74. Ktor fun main() { embeddedServer(Netty, port = 8080, host =

    "0.0.0.0") { routing { get("/html-dsl") { call.respondHtml { body { h1 { +"HTML" } ul { for (n in 1 .. 10) { li { +"$n" } } } } } } } }.start(wait = true) } kotlinx.html Ktor’s routing
  75. final ClientBuilder builder = new ClientBuilder(); builder.setFirstName("Anton"); builder.setLastName("Arhipov"); final TwitterBuilder

    twitterBuilder = new TwitterBuilder(); twitterBuilder.setHandle("@antonarhipov"); builder.setTwitter(twitterBuilder.build()); final CompanyBuilder companyBuilder = new CompanyBuilder(); companyBuilder.setName("JetBrains"); companyBuilder.setCity("Tallinn"); builder.setCompany(companyBuilder.build()); final Client client = builder.build(); System.out.println("Created client is: " + client);
  76. val builder = ClientBuilder() builder.firstName = "Anton" builder.lastName = "Arhipov"

    val twitterBuilder = TwitterBuilder() twitterBuilder.handle = "@antonarhipov" builder.twitter = twitterBuilder.build() val companyBuilder = CompanyBuilder() companyBuilder.name = "JetBrains" companyBuilder.city = "Tallinn" builder.company = companyBuilder.build() val client = builder.build() println("Created client is: $client")
  77. val client = ClientBuilder().apply { firstName = "Anton" lastName =

    "Arhipov" twitter = TwitterBuilder().apply { handle = "@antonarhipov" }.build() company = CompanyBuilder().apply { name = "JetBrains" city = "Tallinn" }.build() }.build() println("Created client is: $client")
  78. val client = ClientBuilder().apply { firstName = "Anton" lastName =

    "Arhipov" twitter = TwitterBuilder().apply { handle = "@antonarhipov" }.build() company = CompanyBuilder().apply { name = "JetBrains" city = "Tallinn" }.build() }.build() println("Created client is: $client")
  79. val client = ClientBuilder().apply { firstName = "Anton" lastName =

    "Arhipov" twitter = TwitterBuilder().apply { handle = "@antonarhipov" }.build() company = CompanyBuilder().apply { name = "JetBrains" city = "Tallinn" }.build() }.build() println("Created client is: $client")
  80. val client = ClientBuilder().apply { firstName = "Anton" lastName =

    "Arhipov" twitter = TwitterBuilder().apply { handle = "@antonarhipov" }.build() company = CompanyBuilder().apply { name = "JetBrains" city = "Tallinn" }.build() }.build() println("Created client is: $client") fun client(c: ClientBuilder.() -> Unit): Client { val builder = ClientBuilder() c(builder) return builder.build() } fun ClientBuilder.company(block: CompanyBuilder.() -> Unit) { company = CompanyBuilder().apply(block).build() } fun ClientBuilder.twitter(block: TwitterBuilder.() -> Unit) { twitter = TwitterBuilder().apply(block).build() }
  81. fun client(c: ClientBuilder.() -> Unit): Client { val builder =

    ClientBuilder() c(builder) return builder.build() } fun ClientBuilder.company(block: CompanyBuilder.() -> Unit) { company = CompanyBuilder().apply(block).build() } fun ClientBuilder.twitter(block: TwitterBuilder.() -> Unit) { twitter = TwitterBuilder().apply(block).build() } val person = person {