Slide 1

Slide 1 text

Expressive Kotlin Duncan McGregor @duncanmcg Nat Pryce @natpryce

Slide 2

Slide 2 text

@

Slide 3

Slide 3 text

Expressive? There are two ways to write code: write code so simple there are obviously no bugs in it, or write code so complex that there are no obvious bugs in it. Tony Hoare The heart of software is its ability to solve domain-related problems for its user. Eric Evans

Slide 4

Slide 4 text

Scope Not full blown DSL design Simple things you can do right away Small transformations that gradually improve the entire codebase

Slide 5

Slide 5 text

Extension functions (as usually described) Additional methods defined on types we don’t own. Before val name = mandatoryChild(node, "name") fun mandatoryChild(node: JsonNode, name: String): JsonNode = node.get(name) ?: throw MissingPropertyException(name) After val name = node.mandatoryChild(“name”) fun JsonNode.mandatoryChild(name: String): JsonNode = get(name) ?: throw MissingPropertyException(name)

Slide 6

Slide 6 text

Local extension functions for local logic Domain-specific extensions defined close to their use Before validateAddress(addressFrom(jsonNode)) After validateAddress(jsonNode.toAddress()) private fun JsonNode.toAddress() = Address( getRequired("street").asText(), getRequired("town").asText(), get("postcode")?.asPostCode(), getRequired("country").asCountryCode())

Slide 7

Slide 7 text

Extension functions chain nicely Before enableCheckbox(isFreeDelivery(addressFrom(jsonNode))) After enableCheckbox(jsonNode.toAddress().isFreeDelivery())

Slide 8

Slide 8 text

Extension functions allow concise names Before if (userCanEditSubmission(currentUser, submission)) { ... } After if (currentUser.canEdit(submission)) { ... }

Slide 9

Slide 9 text

Class cluttered with application functionality class Address( … ) { fun isInCountry(county: Country) = … fun isFreeDelivery() = … fun somethingTheUserInterfaceNeeds() = … fun somethingTheDatabaseNeeds() = … } Extension functions on our own types

Slide 10

Slide 10 text

Extension functions on our own types Fundamental abstraction class Address( … ) { fun isInCountry( … ) } Logic specific to a part of the business domain fun Address.isFreeDelivery() = UI Module fun Address.somethingTheUserInterfaceNeeds() = … Persistence Module fun Address.somethingTheDatabaseNeeds() = …

Slide 11

Slide 11 text

Extension functions vs explaining variables Before if (addressNodes.any { isFreeDelivery(addressFrom(it)) }) { Explaining variable val freeDeliveryAvailable = addressNodes.any { isFreeDelivery(addressFrom(it)) } if (freeDeliveryAvailable) { ... Extension function if (addressNodes.allowFreeDelivery()) { ... private inline fun List.allowFreeDelivery() = any { it.toAddress().isFreeDelivery() }

Slide 12

Slide 12 text

Descriptive names instead of direct calls to copy(). Before val newState = submission.copy(files=submission.files + newFile) After val newState = submission.plusFile(newFile) ... fun Submission.withNoFiles() = copy(files=emptyList()) fun Submission.withFile(f: File) = copy(files=listOf(f)) fun Submission.plusFile(f: File) = copy(files=files + f) Extension functions of data classes Use a common naming convention across the code base. E.g. "withXxx" replaces, "plusXxx" adds, "minusXxx" removes.

Slide 13

Slide 13 text

Parameterise, rather than extend Before val rootUri = config.getURI("server") val accessToken = config.getString("accessToken") val timeout = config.getLong("timeout") After val rootUri = config.get(uri, "server") val accessToken = config.get(string, "accessToken") val timeout = config.get(long, "timeout") object uri : Parser { override fun parse(s: String): URI? = try { URI(s) } catch (e: URISyntaxException) { null } override val name = "URI" }

Slide 14

Slide 14 text

The "M" word Before fun getOrder(request: HttpRequest): HttpResponse = request.parameter("o", ::asOrderId) { orderId -> request.authenticate { accessToken -> lookupOrder(orderId, accessToken) { order -> JsonResponse(order.toJson()) } } } The "Arrow" anti-pattern http://c2.com/cgi/wiki?ArrowAntiPattern

Slide 15

Slide 15 text

The "M" word After fun getOrder(request: HttpRequest): HttpResponse = HttpResult.apply(this::lookupOrder, request.parameter("o", ::asOrderId), request.authenticate() ) .map { it.toJson() } .end { json -> JsonResponse(json) }

Slide 16

Slide 16 text

The "M" word Under the hood fun getOrder(request: HttpRequest): HttpResponse { val orderId: HttpResult = request.parameter("o", ::asOrderId) val accessToken: HttpResult = request.authenticate() val order: HttpResult = HttpResult.apply(this::lookupOrder, orderId, accessToken) return order .map { it.toJson() } .end { json -> JsonResponse(json) } }

Slide 17

Slide 17 text

The "M" word Under the hood sealed class HttpResult: Value() { abstract fun map(fn: (T) -> U): HttpResult abstract fun flatMap(fn: (T) -> HttpResult): HttpResult abstract fun then(fn: (T) -> HttpHandler): HttpResponse ... class Success(val value: T): HttpResult() ... class Failure(val response: HttpResponse): HttpResult() ... companion object { fun apply(fn: (A, B) -> R, a: HttpResult, b: HttpResult) = a.flatMap { aValue -> b.flatMap { bValue -> HttpResult.of(fn(aValue, bValue)) } } } }

Slide 18

Slide 18 text

The "M" word Use the same method names as Kotlin's collections: "map", "flatMap", "filter", ...? Or something domain-specific? After fun getOrder(request: HttpRequest): HttpResponse = HttpResult.apply(this::lookupOrder, request.parameter("o", ::asOrderId), request.authenticate() ) .map { it.toJson() } .end { json -> JsonResponse(json) } ?

Slide 19

Slide 19 text

Capture names from reified types Before object uri : Parser { override fun parse(s: String) = try { URI(s) } catch (e: URISyntaxException) { null } override val name = "URI" } After val uri = parser { try { URI(it) } catch (e: URISyntaxException) { null } } inline fun parser(crossinline fn: (String)->T?) = object : Parser { override fun parse(s: String) = fn(s) override val name = T::class.simpleName.orEmpty() }

Slide 20

Slide 20 text

No content

Slide 21

Slide 21 text

Single expression addicts fun operationFor(query: (String) -> QueryBuilder) = (fun(search: String, client: HttpElasticClient) = client.query(this, query(search))) class Index( val suggestionQuery: (String) -> QueryBuilder, val findQuery: (String) -> QueryBuilder) { val suggestions = operationFor(suggestionQuery) val findResults = operationFor(findQuery) } val suggestions = index.suggestions(“search term”, client)

Slide 22

Slide 22 text

Infixation val config = ConfigurationProperties.systemProperties() overriding EnvironmentVariables() overriding ConfigurationProperties.fromResource("default.conf") infix fun Configuration.overriding(defaults: Configuration) = Override(this, defaults) (n) An unhealthy obsession with infix functions.

Slide 23

Slide 23 text

Operator punning Prefer clearly named functions or methods even though longer val submissionId = PathElement(::SubmissionId) val fileId = PathElement(::FileId) val submissionPath = "submission"/submissionId val submissionFilePath = submissionPath/"file"/fileId WTF?!?

Slide 24

Slide 24 text

Conclusions

Slide 25

Slide 25 text

Thanks! Questions? are hiring http://sndigital.springernature.com