Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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?

Slide 3

Slide 3 text

@

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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.

Slide 8

Slide 8 text

Conversion demo

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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.

Slide 11

Slide 11 text

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 )

Slide 12

Slide 12 text

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!

Slide 13

Slide 13 text

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? ?

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 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 22

Slide 22 text

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

Slide 23

Slide 23 text

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() = …

Slide 24

Slide 24 text

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)

Slide 25

Slide 25 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 26

Slide 26 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 27

Slide 27 text

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 }

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

Rabbit Holes ●Increasing expressiveness drags you in

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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?!?

Slide 32

Slide 32 text

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.

Slide 33

Slide 33 text

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!

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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?

Slide 36

Slide 36 text

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