Slide 1

Slide 1 text

@antonarhipov Idiomatic Kotlin

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

Top-Level Functions & Extensions Scope Functions Default argument values and named parameters Expressions Null-safety Type-safe builders, a.k.a DSL

Slide 5

Slide 5 text

Top-Level (Extension) Functions

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

fun main() { doSomething() } fun doSomething() { doMoreStuff( : : finishWork) } fun doMoreStuff(callback: () -> Unit) { callback() } fun finishWork() { TODO("Not implemented yet") } Just functions, no classes!

Slide 10

Slide 10 text

Expressions & composition

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

Don’t create classes just to hold functions!

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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 20

Slide 20 text

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 21

Slide 21 text

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 22

Slide 22 text

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

Slide 23

Slide 23 text

+

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

fun findMessageById(id: String) = db.query("select * from messages where id = ?", id ) { rs, _ -> Message(rs.getString("id"), rs.getString("text")) } fun JdbcOperations.query(sql: String, vararg args: Any, function: (ResultSet, Int) -> T): List = query(sql, RowMapper { rs, i -> function(rs, i) }, *args) Extension function!

Slide 33

Slide 33 text

Functions, Extensions, Single-expression functions, Trailing lambda, Varargs, SAM conversion

Slide 34

Slide 34 text

Scope functions

Slide 35

Slide 35 text

No content

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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 38

Slide 38 text

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.apply(block: T.() -> Unit): T { block() return this }

Slide 39

Slide 39 text

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.apply(block: T.() -> Unit): T { block() return this } Lambda with receiver

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

val order = retrieveOrder() if (order != null){ processCustomer(order.customer) } retrieveOrder() ?. let { processCustomer(it.customer) } retrieveOrder() ?. customer ?. let( :: processCustomer) or ? . let

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

No content

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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!

Slide 50

Slide 50 text

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!

Slide 51

Slide 51 text

Default argument values and named parameters

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

Default argument values diminish the need for overloading in most cases

Slide 59

Slide 59 text

Named parameters is a necessary tool for working with default argument values Default argument values diminish the need for overloading in most cases

Slide 60

Slide 60 text

Expressions if when try

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

fun adjustSpeed(weather: Weather) = if (weather is Rainy) Safe() else Calm() What does the function return?

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

No content

Slide 76

Slide 76 text

No content

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

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

Slide 82

Slide 82 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 expression

Slide 83

Slide 83 text

The when expression is your friend!

Slide 84

Slide 84 text

val condition: Boolean = expression() when(condition) { true -> doThis() false -> doThat() } if (condition) { doThis() } else { doThat() } if when

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

Null-safety

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

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

Slide 90

Slide 90 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 91

Slide 91 text

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

Slide 92

Slide 92 text

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

Slide 93

Slide 93 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 94

Slide 94 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 95

Slide 95 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 96

Slide 96 text

No content

Slide 97

Slide 97 text

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

Slide 98

Slide 98 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 99

Slide 99 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 100

Slide 100 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 101

Slide 101 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 102

Slide 102 text

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

Slide 103

Slide 103 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 104

Slide 104 text

Domain Specific Languages

Slide 105

Slide 105 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 106

Slide 106 text

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

Slide 107

Slide 107 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 108

Slide 108 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 109

Slide 109 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 110

Slide 110 text

Lambda with receiver T.() -> Unit

Slide 111

Slide 111 text

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 112

Slide 112 text

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 113

Slide 113 text

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 114

Slide 114 text

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 115

Slide 115 text

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 116

Slide 116 text

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

Slide 117

Slide 117 text

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 {

Slide 118

Slide 118 text

https://speakerdeck.com/antonarhipov @antonarhipov https://github.com/antonarhipov