Slide 1

Slide 1 text

@antonarhipov Idiomatic Kotlin from formatting to DSLs

Slide 2

Slide 2 text

Agenda • Expressions • Examples from standard library • DSL

Slide 3

Slide 3 text

Anton Arhipov @antonarhipov Developer Advocate @ JetBrains

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

Idiomatic - using, containing, or denoting expressions that are natural to a native speaker In case of a programming language: •Conforms to a commonly accepted style •E ff ectively uses features of the programming language

Slide 9

Slide 9 text

No content

Slide 10

Slide 10 text

Expressions try, if, when

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

fun adjustSpeed(weather: Weather) = if (weather is Rainy) Safe() else Calm()

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

fun adjustSpeed(weather: Weather) = if (weather is Rainy) Safe() else Calm()

Slide 28

Slide 28 text

fun adjustSpeed(weather: Weather) = if (weather is Rainy) Safe() else Calm()

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

Use try as expression fun tryParse(number: String) : Int? { val n = try { Integer.parseInt(number) } catch (e: NumberFormatException) { null } println(n) return n }

Slide 37

Slide 37 text

Use elvis operator class Person(val name: String?, val age: Int?) val p = retrievePerson() ?: Person()

Slide 38

Slide 38 text

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") }

Slide 39

Slide 39 text

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") }

Slide 40

Slide 40 text

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") }

Slide 41

Slide 41 text

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") }

Slide 42

Slide 42 text

Nullability

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

Consider using null-safe call val order = retrieveOrder() val city = order ?. customer ? . address ?. city ?: throw IllegalArgumentException("Invalid Order")

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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) } }

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

Consider using ?.let for null-checks val order = retrieveOrder() if (order != null){ processCustomer(order.customer) }

Slide 50

Slide 50 text

Consider using ?.let for null-checks val order = retrieveOrder() if (order != null){ processCustomer(order.customer) } retrieveOrder() ?. let { processCustomer(it.customer) } retrieveOrder() ?. customer ?. let { :: processCustomer } or

Slide 51

Slide 51 text

Consider using ?.let for null-checks val order = retrieveOrder() if (order != null){ processCustomer(order.customer) } retrieveOrder() ?. let { processCustomer(it.customer) } No need for an extra variable retrieveOrder() ?. let { processCustomer(it.customer) } retrieveOrder() ?. customer ?. let { :: processCustomer } or

Slide 52

Slide 52 text

Consider using ?.let for null-checks val order = retrieveOrder() if (order != null){ processCustomer(order.customer) } retrieveOrder() ?. let { processCustomer(it.customer) } retrieveOrder() ?. let { processCustomer(it.customer) } retrieveOrder() ?. customer ?. let { :: processCustomer } or

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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 }

Slide 55

Slide 55 text

Use range checks instead of comparison pairs fun isLatinUppercase(c: Char) = c > = 'A' && c < = 'Z'

Slide 56

Slide 56 text

Use range checks instead of comparison pairs fun isLatinUppercase(c: Char) = c > = 'A' && c < = 'Z'

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

Ranges in loops fun main(args: Array) { 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]}") }

Slide 63

Slide 63 text

Ranges in loops fun main(args: Array) { 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") }

Slide 64

Slide 64 text

Classes and Functions

Slide 65

Slide 65 text

Don’t create classes just to hold functions class StringUtils { companion object { fun isPhoneNumber(s: String) = s.length = = 7 & & s.all { it.isDigit() } } }

Slide 66

Slide 66 text

Don’t create classes just to hold functions 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() } }

Slide 67

Slide 67 text

Don’t create classes just to hold functions 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() }

Slide 68

Slide 68 text

Use extension functions 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() }

Slide 69

Slide 69 text

Extension or a member? https://kotlinlang.org/docs/coding-conventions.html#extension-functions •Use extension functions liberally. •If a function works primarily on an object, consider making it an extension with that object as a receiver. •Minimize API pollution, restrict the visibility. •As necessary, use local extension functions, member extension functions, or top-level extension functions with private visibility.

Slide 70

Slide 70 text

Use default values instead of overloading class Phonebook { fun print() { print(",") } fun print(columnSeparator: String) {} } fun main(args: Array) { Phonebook().print("|") }

Slide 71

Slide 71 text

Use default values instead of overloading class Phonebook { fun print() { print(",") } fun print(columnSeparator: String) {} } fun main(args: Array) { Phonebook().print("|") } class Phonebook { fun print(separator: String = ",") {} fun someFun(x: Int) {} } fun main(args: Array) { Phonebook().print(separator = "|") }

Slide 72

Slide 72 text

Return multiple values using data classes fun namedNum(): Pair = 1 to "one" // same but shorter fun namedNum2() = 1 to "one" fun main(args: Array) { val pair = namedNum() val number = pair.first val name = pair.second }

Slide 73

Slide 73 text

Return multiple values using data classes fun namedNum(): Pair = 1 to "one" // same but shorter fun namedNum2() = 1 to "one" fun main(args: Array) { val pair = namedNum() val number = pair.first val name = pair.second } data class GameResult( val rank: Int, val name: String ) fun namedNum() = GameResult(1, "Player 1") fun main(args: Array) { val (rank, name) = namedNum() println("$name, rank $rank") }

Slide 74

Slide 74 text

Return multiple values using data classes data class GameResult( val rank: Int, val name: String ) fun namedNum() = GameResult(1, "Player 1") fun main(args: Array) { val (rank, name) = namedNum() println("$name, rank $rank") } GameResult var1 = namedNum(); int var2 = var1.component1(); String var3 = var1.component2();

Slide 75

Slide 75 text

Destructuring in loops fun printMap(map: Map) { for (item in map.entries) { println("${item.key} -> ${item.value}") } }

Slide 76

Slide 76 text

Destructuring in loops fun printMap(map: Map) { for (item in map.entries) { println("${item.key} -> ${item.value}") } } fun printMap(map: Map) { for ((key, value) in map) { println("$key -> $value") } }

Slide 77

Slide 77 text

Destructuring in lists data class NameExt( val name: String, val ext: String? ) fun splitNameExt(filename: String): NameExt { if ('.' in filename) { val parts = filename.split('.', limit = 2) return NameExt(parts[0], parts[1]) } return NameExt(filename, null) } fun splitNameAndExtension(filename: String): NameExt { if ('.' in filename) { val (name, ext) = filename.split('.', limit = 2) return NameExt(name, ext) } return NameExt(filename, null) }

Slide 78

Slide 78 text

No content

Slide 79

Slide 79 text

Use type aliases for functional types class Event class EventDispatcher { fun addClickHandler(handler: (Event) -> Unit) {} fun removeClickHandler(handler: (Event) -> Unit) {} }

Slide 80

Slide 80 text

Use type aliases for functional types class Event class EventDispatcher { fun addClickHandler(handler: (Event) -> Unit) {} fun removeClickHandler(handler: (Event) -> Unit) {} } typealias ClickHandler = (Event) -> Unit class EventDispatcher { fun addClickHandler(handler: ClickHandler) { } fun removeClickHandler(handler: ClickHandler) { } }

Slide 81

Slide 81 text

Standard Library

Slide 82

Slide 82 text

Verify parameters using require() class Person( val name: String?, val age: Int ) fun processPerson(person: Person) { if (person.age < 18) { throw IllegalArgumentException("Adult required") } }

Slide 83

Slide 83 text

Verify parameters using require() class Person( val name: String?, val age: Int ) fun processPerson(person: Person) { if (person.age < 18) { throw IllegalArgumentException("Adult required") } } fun processPerson(person: Person) { require(person.age > = 18) { "Adult required" } }

Slide 84

Slide 84 text

Select objects by type with filterIsInstance fun findAllStrings(objects: List) = objects.filter { it is String }

Slide 85

Slide 85 text

Select objects by type with filterIsInstance fun findAllStrings(objects: List) = objects.filter { it is String } fun findAllStrings(objects: List) = objects.filterIsInstance()

Slide 86

Slide 86 text

Select objects by type with filterIsInstance fun findAllStrings(objects: List) : List = objects.filter { it is String } fun findAllStrings(objects: List) : List = objects.filterIsInstance()

Slide 87

Slide 87 text

Apply operation to non-null elements mapNotNull data class Result( val data: Any?, val error: String? ) fun listErrors(results: List): List = results.map { it.error }.filterNotNull() fun listErrors(results: List): List = results.mapNotNull { it.errorMessage }

Slide 88

Slide 88 text

compareBy compares by multiple keys class Person( val name: String, val age: Int ) fun sortPersons(persons: List) = persons.sortedWith(Comparator { person1, person2 -> val rc = person1.name.compareTo(person2.name) if (rc != 0) rc else person1.age - person2.age })

Slide 89

Slide 89 text

compareBy compares by multiple keys class Person( val name: String, val age: Int ) fun sortPersons(persons: List) = persons.sortedWith(Comparator { person1, person2 -> val rc = person1.name.compareTo(person2.name) if (rc != 0) rc else person1.age - person2.age }) fun sortPersons(persons: List) = persons.sortedWith(compareBy(Person :: name, Person : : age))

Slide 90

Slide 90 text

groupBy to group elements class Request( val url: String, val remoteIP: String, val timestamp: Long ) fun analyzeLog(log: List) { val map = mutableMapOf> () for (request in log) { map.getOrPut(request.url) { mutableListOf() } .add(request) } }

Slide 91

Slide 91 text

groupBy to group elements class Request( val url: String, val remoteIP: String, val timestamp: Long ) fun analyzeLog(log: List) { val map = mutableMapOf> () for (request in log) { map.getOrPut(request.url) { mutableListOf() } .add(request) } } fun analyzeLog(log: List) { val map = log.groupBy(Request :: url) }

Slide 92

Slide 92 text

Use coerceIn to ensure numbers in range fun updateProgress(value: Int) { val actualValue = when { value < 0 - > 0 value > 100 -> 100 else - > value } } fun updateProgress(value: Int) { val actualValue = value.coerceIn(0, 100) }

Slide 93

Slide 93 text

Initializing objects with apply 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 }

Slide 94

Slide 94 text

Initializing objects with apply 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);

Slide 95

Slide 95 text

Initializing objects with apply 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")

Slide 96

Slide 96 text

Initializing objects with apply 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") 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")

Slide 97

Slide 97 text

Domain Specific Languages

Slide 98

Slide 98 text

“Domain Speci fi c”, i.e. tailored for a speci fi c task

Slide 99

Slide 99 text

“Domain Speci fi c”, i.e. tailored for a speci fi c task Examples: •Compose strings - stringBuilder •Create HTML documents - kotlinx.html •Con fi gure routing logic for a web app - ktor •Generally, build any object graphs. See “type-safe builders”

Slide 100

Slide 100 text

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)

Slide 101

Slide 101 text

kotlinx.html System.out.appendHTML().html { body { div { a("http: // kotlinlang.org") { target = ATarget.blank +"Main site" } } } }

Slide 102

Slide 102 text

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) }

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

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

Slide 105

Slide 105 text

Lambda with receiver T.() -> Unit

Slide 106

Slide 106 text

Build your vocabulary to abstract from scope functions 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")

Slide 107

Slide 107 text

Build your vocabulary to abstract from scope functions 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 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")

Slide 108

Slide 108 text

val client = client { firstName = "Anton" lastName = "Arhipov" twitter { handle = "@antonarhipov" } company { name = "JetBrains" city = "Tallinn" } } println("Created client is: $client") Build your vocabulary to abstract from scope functions 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")

Slide 109

Slide 109 text

https://speakerdeck.com/antonarhipov https://github.com/antonarhipov/idiomatic-kotlin @antonarhipov