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

Kotlin - from basics to DSLs

Kotlin - from basics to DSLs

Anton Arhipov

February 10, 2022
Tweet

More Decks by Anton Arhipov

Other Decks in Technology

Transcript

  1. @antonarhipov Kotlin: from basics to DSLs

  2. @antonarhipov Developer Advocate @ JetBrains Anton Arhipov

  3. 2011 - a new general purpose statically typed alternative language

    for the JVM Later - Android, Kotlin/JS, Kotlin/Native, WASM… 2014 - Kotlin 1.0 2017 - Main language for Android Current: Kotlin 1.6.20
  4. None
  5. None
  6. None
  7. What developers say: Concise & modern Coroutines MPP, Kotlin/JS stdlib

    Null-safety
  8. Concise & modern Just a syntax sugar Sceptic

  9. Concise & modern Just a syntax sugar MPP is not

    ready (and I don’t need it) Sceptic MPP, Kotlin/JS
  10. Concise & modern Just a syntax sugar MPP is not

    ready (and I don’t need it) We have libs for async (RxJava) Sceptic Java 17 will bring Loom!11 MPP, Kotlin/JS Coroutines
  11. Concise & modern Just a syntax sugar MPP is not

    ready (and I don’t need it) We have libs for async (RxJava) Sceptic Java 17 will bring Loom!11 Null-safety can be achieved with Optional MPP, Kotlin/JS Coroutines Null-safety
  12. Concise & modern Just a syntax sugar MPP is not

    ready (and I don’t need it) We have libs for async (RxJava) Sceptic Java 17 will bring Loom!11 Java now has records, sealed classes, and pattern matching Null-safety can be achieved with Optional MPP, Kotlin/JS Coroutines Null-safety
  13. None
  14. None
  15. https://kotlinlang.org/docs/comparison-to-java.html#some-java-issues-addressed-in-kotlin

  16. https://kotlinlang.org/docs/comparison-to-java.html#what-java-has-that-kotlin-does-not

  17. https://kotlinlang.org/docs/comparison-to-java.html#what-kotlin-has-that-java-does-not

  18. None
  19. None
  20. None
  21. val person = person {

  22. Top-Level (Extension) Functions

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

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

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

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

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

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

    s.length = = 7 & & s.all { it.isDigit() } } }
  29. 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() } }
  30. 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() }
  31. 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() }
  32. Extension or a member? https://kotlinlang.org/docs/coding-conventions.html#extension-functions Use extension functions liberally Restrict

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

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

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

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

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

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

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

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

    = ?", id ) { rs, _ -> Message(rs.getString("id"), rs.getString("text")) }
  42. 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!
  43. Scope functions apply, let, run, also, with

  44. None
  45. 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
  46. 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 }
  47. 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 }
  48. 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
  49. ?.let val order = retrieveOrder() if (order != null){ processCustomer(order.customer)

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

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

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

    } Don’t overuse the scope functions!
  54. 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!
  55. 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 :)
  56. Default argument values and named parameters

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

    Boolean){ } Function overloading
  58. 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
  59. 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
  60. 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")
  61. 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
  62. 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")
  63. Default argument values diminish the need for overloading in most

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    Calm() What does the function return?
  79. 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
  80. fun adjustSpeed(weather: Weather) = if (weather is Rainy) Safe() else

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

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

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

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

    Weather() fun adjustSpeed(weather: Weather) = when (weather) { is Rainy -> Safe() is Sunny -> TODO() }
  85. 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
  86. Use try as expression body fun tryParse(number: String) : Int?

    { try { return Integer.parseInt(number) } catch (e: NumberFormatException) { return null } }
  87. Use try as expression body fun tryParse(number: String) = try

    { Integer.parseInt(number) } catch (e: NumberFormatException) { null }
  88. Null-safety

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

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

    null fun main() { val n: Nullable? = createNullable() n.someFunction() }
  91. 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
  92. Consider using null-safe call val order = retrieveOrder() val city

    = order ?. customer ? . address ?. city
  93. Consider using null-safe call val order = retrieveOrder() val city

    = order ?. customer ? . address ?. city ?: throw IllegalArgumentException("Invalid Order")
  94. 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
  95. 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) } }
  96. 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
  97. None
  98. Use elvis operator class Person(val name: String?, val age: Int?)

    val p = retrievePerson() ?: Person()
  99. 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") }
  100. 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") }
  101. 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") }
  102. 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") }
  103. Consider using safe cast for type checking override fun equals(other:

    Any?) : Boolean { val command = other as Command return command.id == id }
  104. 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 }
  105. None
  106. Can I write enterprise apps in Kotlin?

  107. Can I write enterprise apps in Kotlin? What framework(s) should

    I use? Libraries, build tools, etc?
  108. Kotlin vs Java

  109. None
  110. None
  111. None
  112. Kotlin for backend development

  113. Domain Specific Languages

  114. Domain-speci fi c, i.e.

  115. tailored for the speci fi c task Domain-speci fi c,

    i.e.
  116. External

  117. External VS Internal

  118. External VS Internal

  119. External VS Internal

  120. External VS Internal

  121. External VS Internal

  122. Internal

  123. 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)
  124. kotlinx.html System.out.appendHTML().html { body { div { a("http: // kotlinlang.org")

    { target = ATarget.blank +"Main site" } } } }
  125. 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) }
  126. 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
  127. 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
  128. They all look similar!

  129. foo { bar { baz = "Hello!" qux = quux

    { corge = "Blah" } } }
  130. foo { bar { baz = "Hello!" qux = quux

    { corge = "Blah" } } }
  131. foo { bar { baz = "Hello!" qux = quux

    { corge = "Blah" } } }
  132. foo { bar { baz = "Hello!" qux = quux

    { corge = "Blah" } } }
  133. foo { bar(grault = 1) { baz = "Hello!" qux

    = quux { corge = "Blah" } } }
  134. foo { bar(grault = 1) { baz = "Hello!" qux

    = quux { corge = Hello() } } }
  135. foo { bar(grault = 1) { this: Bar baz =

    "Hello!" qux = quux { this: Quux corge = Hello() } } }
  136. Let’s write some code! https://github.com/antonarhipov/kotlin-dsl-examples

  137. Let’s write some code! https://github.com/antonarhipov/kotlin-dsl-examples

  138. Type-safe builders https://kotlinlang.org/docs/reference/type-safe-builders.html

  139. Lambda with receiver T.() -> Unit

  140. 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);
  141. 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")
  142. 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")
  143. 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")
  144. 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")
  145. 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() }
  146. 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 {
  147. https://speakerdeck.com/antonarhipov @antonarhipov