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

Adopting Kotlin, Or: How we learned to stopped worrying and love the null

Adopting Kotlin, Or: How we learned to stopped worrying and love the null

A case study presented by Duncan McGregor & Nat Pryce at SPA 2017

Nat Pryce

June 27, 2017
Tweet

More Decks by Nat Pryce

Other Decks in Programming

Transcript

  1. Adopting Kotlin
    Or: How we learned to stopped worrying and love the null
    Duncan McGregor & Nat Pryce

    View Slide

  2. What is Scala?
    ● A statically-typed OO/functional hybrid language
    running on the JVM
    ● A hell of a compiler
    ● An impressive runtime
    ● Still evolving
    Kotlin?

    View Slide

  3. @

    View Slide

  4. What were we building at Springer Nature?
    ●Server-side JVM webapps, hosted on Undertow
    ●Nat - 6 devs, Postgres, RabbitMQ
    ●Duncan - 4 devs, Elastic Search and Postgres
    ●Algorithmically uninteresting
    ●TDD, mostly XP
    ●IntelliJ, Gradle, Git
    ●Springer Nature’s own CD pipeline to Cloud Foundry

    View Slide

  5. Why did we adopt Kotlin?
    Because
    ●we like / needed to run on the JVM
    ●it’s so hard to do simple things in Java
    ●we’re too old to cope with dynamic languages
    ●we value excellent tool support
    ●everything else was worse

    View Slide

  6. Why Might You Migrate?
    ●Productivity
    ● FP benefits in algorithm design
    ● Immutable throughput for multi-core
    ● Expressiveness
    ● The Python Paradox

    View Slide

  7. Coming up...
    What we did Overenthusiasm
    Culture Shock
    Gradually converting a Java
    application to Kotlin
    without slowing delivery.
    We went too far so that
    you don't have to.
    Kotlin is a foreign
    country; they do things
    differently there.

    View Slide

  8. Conversion demo

    View Slide

  9. How did we migrate?
    ●Build in both Java and
    ●New classes in Scala
    ●Migrate existing classes case-by-case
    ■ “Convert File to Kotlin”
    ■ Move to functional style
    ■ Embrace null
    ■ Adopt Kotlin idioms
    This is also a good model for learning Scala
    Kotlin
    Kotlin
    Kotlin

    View Slide

  10. Code layout and organisation
    The Kotlin style guide is not very extensive
    It seems trivial, but inconsistent conventions became annoying
    ● Code layout
    ● Code organisation
    ● Visibility modifiers
    Each project developed its own conventions … eventually.
    They were all slightly different.

    View Slide

  11. It took us time to accept using null
    data class SubmissionDraft(
    override val id: DraftId,
    override val submissionStatus: SubmissionStatus,
    override val createdTimestamp: Instant,
    override val submittedTimestamp: Instant? = null,
    override val submissionId: SubmissionId?,
    override val decisions: UserDecisions
    )

    View Slide

  12. Extension functions on nullable types
    "Elvis has left the building!"
    This can be null in an extension method of a nullable type!
    fun String?.orEmpty() = if (this == null) "" else this
    fun List?.orEmpty() = this ?: emptyList()
    Beware: toString is an extension method of a nullable type!
    val maybeCount: Int? = …
    maybeCount?.toString() // returns null or the count
    maybeCount.toString() // returns "null" or the count
    The type checker won't help: String is a subtype of String?
    Impossible to spot the difference in test diagnostics!

    View Slide

  13. Exceptions are not type checked!
    "Failure is not an option!"
    sealed class Result
    data class Failure( val value: ERR) : Result()
    data class Success(val value: T) : Result()
    inline fun Result.map(f: (T) -> U): Result = ...
    inline fun Result.flatMap(f: ( T) -> Result): Result = ...
    inline fun Result.forEach(f: (T) -> Unit): Unit = ...
    inline fun Result.orElse(f: (ErrorCode) -> T) = ...
    inline fun T?.asResultOr(errorCodeFn: () -> ErrorCode): Result< T> = ...
    ... etc.
    Use the same method names as Kotlin's collections: "map", "flatMap", "filter", ...?
    Or something domain-specific?
    ?

    View Slide

  14. Exceptions are not type checked!
    Technical
    error?
    Further
    action?
    Catch as close
    as possible to
    failing call.
    Return an error
    code in a
    Result.failure
    Translate to HTTP
    response in the
    web layer
    Let the exception
    propagate – the
    web layer will send
    a 500 response
    Yes
    No
    Yes
    No

    View Slide

  15. Enforced by fuzz testing
    @Test fun `reports all parse errors with a JSON Pointer`() {
    val validJson = exampleAmendmentRequest.toJson()
    val mutants = random.mutants(
    defaultJsonMutagens().forJackson(), 500, validJson)
    mutants.forEach { mutantJson ->
    val result = format.parseJson(mutantJson)
    // no exception thrown
    if (result is Failure)
    assertThat(result.value, isA(
    has(InvalidJson::pointer, present())))
    }
    }
    https://github.com/npryce/snodge

    View Slide

  16. Result instead of exceptions
    fun sendToEjp(draft: SubmissionDraft): Result =
    journals.getByPCode(draft.pCode)
    .flatMap { journal ->
    journal.ejpJournalId
    .asResultOr { PeerReviewSystemNotSupported(ejp, draft.pCode) }
    .flatMap { ejpJournalId ->
    payloadConverter.convertForCreate(draft)
    .flatMap { requestJson ->
    val uri = EjpUrlRoutes.create.path(ejpJournalId)
    sendCreate(uri, payload)
    .flatMap { responseJson ->
    responseJson.toSubmissionId()
    }
    }
    }
    }
    Type safe, but...

    View Slide

  17. Accept early returns in functional code
    inline fun Result.onFailure(handler: (Failure) -> Nothing): T =
    when (this) {
    is Failure -> handler( this)
    is Success -> value
    }
    fun create(draft: SubmissionDraft): Result {
    val journal = journals.getByPCode(draft. pCode)
    ?: return Failure(JournalNotFound(draft. pCode))
    val journalId = journal. ejpJournalId
    ?: return Failure(PeerReviewSystemMisconfiguration())
    val ejpPayload = ejpPayloadConverter.convertForCreate(draft)
    .onFailure { return it }
    val uri = EjpUrlRoutes. createPath.path(journalId)
    val responseJson = sendCreate(uri, payload)
    .onFailure { return it }
    return responseJson.toSubmissionId()
    }

    View Slide

  18. So Far So Good
    ●At this point you just have a better Java
    ● More consistent
    ● Default immutability
    ● Better collections
    ● Less noisy
    ● More bang per line of code
    ● Fast

    View Slide

  19. Except
    ●The build is much slower
    ●Tool support is worse
    ●Debugging is harder
    ●Error messages are sometimes cryptic
    ●Scala runtime source is opaque
    ●Scala is evolving

    View Slide

  20. The Next Step
    ●Scala tools for making code more expressive
    ● Mixin traits
    ● Pattern matching
    ● Implicit conversions / parameters
    ● Operator overloading
    ● AST Macros
    ● Compiler plugins

    View Slide

  21. 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)

    View Slide

  22. Class cluttered with application functionality
    class Address( … ) {
    fun isInCountry(county: Country) = …
    fun isFreeDelivery() = …
    fun somethingTheUserInterfaceNeeds() = …
    fun somethingTheDatabaseNeeds() = …
    }
    Or application functionality in functions
    enable(checkbox, isFreeDelivery(preferredAddress(customer)))
    We extend our own types

    View Slide

  23. Local extension functions for local logic
    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() = …

    View Slide

  24. Extension functions allow concise names
    Nested functions can be verbose and hard to read
    checkbox.enabled =
    addressIsFreeDelivery(customerPreferredAddress(customer))
    Extension functions chain nicely
    checkbox.enabled = customer.preferredAddress.isFreeDelivery
    (And make it much easier to deal with null references)

    View Slide

  25. 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.

    View Slide

  26. 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"
    }

    View Slide

  27. Extension functions can be instance methods!
    A data type that will be extended
    data class Address(..., val postCode: PostCode)
    Extension function used in a method of a class and defined in that class
    class PlaceOrder(
    private val delivery: DeliveryApiClient
    ): UserAction {
    override fun invoke(order: Order) {
    if (order.deliveryAddress.allowsFreeDelivery()) ...
    ...
    }
    private fun Address.allowsFreeDelivery() =
    delivery.methodsFor(postCode).find { it.isFree } != null
    }

    View Slide

  28. Nesting extension methods, extension
    method literals, extension method instance
    methods, local extension methods, ...

    View Slide

  29. Rabbit Holes
    ●Increasing expressiveness drags you in

    View Slide

  30. Infixation
    val config = EnvironmentVariables overriding
    ConfigurationProperties.fromResource("default.conf")
    infix fun Configuration.overriding(defaults: Configuration) =
    Override(this, defaults)
    (n) An unhealthy obsession with infix functions.
    fun Author.toJson(): JsonNode = obj(
    "name" of name,
    "affiliation" of affiliation.toJson(),
    "email" of emailAddress
    )
    Unnecessary (but published, so can't be changed without breaking code)
    Useful

    View Slide

  31. Operator punning
    Prefer conventional operators or clearly named functions
    val submissionId = PathElement(::SubmissionId)
    val fileId = PathElement(::FileId)
    val submissionPath = "submission"/submissionId
    val submissionFilePath = submissionPath/"file"/fileId
    WTF?!?

    View Slide

  32. Spaces in Identifiers
    private fun String.`fromCamelCaseTo space separated`() =
    this.replace(wordBoundaryRegex, "$1 $2")
    private val wordBoundaryRegex = Regex("(\\p{Ll})(\\p{Lu})")
    @Test
    fun `an author cannot edit another author's submission`() …
    Too jokey & makes client code hard to read
    Ok – test names act as documentation in IDE views & no client code
    This limited use case is now supported by IntelliJ's rename refactoring.

    View Slide

  33. Adoption in other projects
    ● Kotlin has spread very quickly throughout the organisation
    ● Java, Scala and Python developers pick it up very quickly
    ● People have just been writing code with no respect for our
    authoritah!

    View Slide

  34. Summary
    Don't go overboard with
    Kotlin's "cool" features
    You can start right away!
    No need for a big-bang port
    Be aware of how
    Kotlin differs from Java

    View Slide

  35. Summary
    ●Simultaneously impressed and horrified
    ●Addictive
    ●I wouldn’t hesitate to take a Scala contract
    ●Starting a Scala project is less clear-cut
    ●You can live without cutting edge IDE support (?)
    ●Suitable for a bleeding edge team
    ●Is something simpler trying to get out?

    View Slide

  36. The beginning...
    (who are hiring in London & Berlin)
    https://www.springernature.com/gp/group/careers
    Thanks to

    View Slide