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?
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
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
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.
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
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.
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 )
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!
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? ?
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
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
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() }
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
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
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)
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
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() = …
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)
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.
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" }
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 }
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
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?!?
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.
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!
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
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?