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
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.
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
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.
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 )
This can be null in an extension method of a nullable type! fun String?.orEmpty() = if (this == null) "" else this fun <T> List<T>?.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!
sealed class Result<out T, out ERR: Any > data class Failure<out ERR: Any>( val value: ERR) : Result<Nothing,ERR>() data class Success<out T>(val value: T) : Result<T, Nothing>() inline fun <T, U> Result<T>.map(f: (T) -> U): Result<U> = ... inline fun <T, U> Result<T>.flatMap(f: ( T) -> Result<U>): Result<U> = ... inline fun <T> Result<T>.forEach(f: (T) -> Unit): Unit = ... inline fun <T> Result<T>.orElse(f: (ErrorCode) -> T) = ... inline fun <T> T?.asResultOr(errorCodeFn: () -> ErrorCode): Result< T> = ... ... etc. Use the same method names as Kotlin's collections: "map", "flatMap", "filter", ...? Or something domain-specific? ?
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
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<InvalidJson>( has(InvalidJson::pointer, present()))) } } https://github.com/npryce/snodge
(Failure) -> Nothing): T = when (this) { is Failure -> handler( this) is Success -> value } fun create(draft: SubmissionDraft): Result<EjpSubmissionId> { 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() }
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)
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
… ) { fun isInCountry( … ) } Logic specific to a part of the business domain fun Address.isFreeDelivery() = UI Module fun Address.somethingTheUserInterfaceNeeds() = … Persistence Module fun Address.somethingTheDatabaseNeeds() = …
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)
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.
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<Order> { override fun invoke(order: Order) { if (order.deliveryAddress.allowsFreeDelivery()) ... ... } private fun Address.allowsFreeDelivery() = delivery.methodsFor(postCode).find { it.isFree } != null }
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
"$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.
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!
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?