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

Expressive Kotlin at London Kotlin Night

Nat Pryce
October 12, 2016

Expressive Kotlin at London Kotlin Night

Presented with Duncan McGregor at London Kotlin Night, 12/10/2016.

Video online here: https://youtu.be/p-AOjgobGR8

Nat Pryce

October 12, 2016
Tweet

More Decks by Nat Pryce

Other Decks in Programming

Transcript

  1. @

  2. 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
  3. Scope Not full blown DSL design Simple things you can

    do right away Small transformations that gradually improve the entire codebase
  4. 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)
  5. 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())
  6. Class cluttered with application functionality class Address( … ) {

    fun isInCountry(county: Country) = … fun isFreeDelivery() = … fun somethingTheUserInterfaceNeeds() = … fun somethingTheDatabaseNeeds() = … } Extension functions on our own types
  7. 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() = …
  8. 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<JsonNode>.allowFreeDelivery() = any { it.toAddress().isFreeDelivery() }
  9. 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.
  10. 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<URI> { override fun parse(s: String): URI? = try { URI(s) } catch (e: URISyntaxException) { null } override val name = "URI" }
  11. 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
  12. 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) }
  13. The "M" word Under the hood fun getOrder(request: HttpRequest): HttpResponse

    { val orderId: HttpResult<OrderId> = request.parameter("o", ::asOrderId) val accessToken: HttpResult<AccessToken> = request.authenticate() val order: HttpResult<Order> = HttpResult.apply(this::lookupOrder, orderId, accessToken) return order .map { it.toJson() } .end { json -> JsonResponse(json) } }
  14. The "M" word Under the hood sealed class HttpResult<out T>:

    Value() { abstract fun <U> map(fn: (T) -> U): HttpResult<U> abstract fun <U> flatMap(fn: (T) -> HttpResult<U>): HttpResult<U> abstract fun then(fn: (T) -> HttpHandler): HttpResponse ... class Success<out T>(val value: T): HttpResult<T>() ... class Failure(val response: HttpResponse): HttpResult<Nothing>() ... companion object { fun <A, B, R> apply(fn: (A, B) -> R, a: HttpResult<A>, b: HttpResult<B>) = a.flatMap { aValue -> b.flatMap { bValue -> HttpResult.of(fn(aValue, bValue)) } } } }
  15. 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) } ?
  16. Capture names from reified types Before object uri : Parser<URI>

    { 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 <reified T> parser(crossinline fn: (String)->T?) = object : Parser<T> { override fun parse(s: String) = fn(s) override val name = T::class.simpleName.orEmpty() }
  17. 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)
  18. 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.
  19. 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?!?