7 Best Practices and Tips for Using Kotlin: A Guide for Scala Developers

Agoda Engineering
Agoda Engineering & Design
11 min readJun 27, 2023

By Aleksandr Nekrasov

Kotlin is a modern, statically typed programming language that has gained popularity due to its concise syntax, expressiveness, and compatibility with Java. If you are a Scala developer looking to start coding in Kotlin, you have come to the right place. This article will provide seven best practices and tips on becoming a proficient Kotlin developer.

Key Differences Between Kotlin and Scala

As a Scala developer interested in learning Kotlin, it’s essential to understand that while both languages share several similarities, there are key differences to keep in mind.

  • Kotlin emphasizes conciseness, ease of use, and seamless Java interoperability. With improved null safety and official support from Google for Android development, Kotlin has quickly gained traction in various application domains. Although Kotlin supports functional programming, developers from a Scala background may find certain features less advanced than what they’re accustomed to in Scala.
  • While transitioning from Scala to Kotlin, you can expect a smoother learning curve due to Kotlin’s more straightforward syntax. However, be prepared for some adjustments regarding functional programming and advanced type system features, where Scala has a clear advantage.
  • As a Scala developer venturing into Kotlin, it’s crucial to consider factors such as the maturity of the ecosystem, IDE and tooling support, language features, and Kotlin’s growing community. Leveraging the commonalities between the languages, such as their Java interoperability and certain functional programming aspects, can facilitate the learning process.

Now, let’s compare both languages side-by-side.

1. Readability and Conciseness

As a Scala developer, you are already used to working with a language that is expressive and functional. Kotlin is designed to be cleaner and more concise. With features like smart casts, extension functions, and lambda expressions, Kotlin simplifies the code-writing process. Make the most of these features to improve readability and reduce verbosity.

  • Type Inference

Kotlin:

val message = "Hello, World!" // Kotlin infers the type to be string

Scala:

val message = "Hello, World! // Scala infers the type to be string

Both languages utilize type inference to determine variable types, which helps reduce verbosity and improve readability. This makes the code easier to understand and navigate.

  • Smart Casts

Kotlin:

fun printLength(obj: Any) {
if (obj is String) {
// Smart cast: Kotlin automatically casts obj to the String type
println("The length of the string is: ${obj.length}")
} else ...
}

Scala:

  • Data classes vs. Case classes

Kotlin:

data class Person(val name: String, val age: Int)

Scala:

case class Person(name: String, age: Int)

While Kotlin and Scala provide features to reduce boilerplate code (data classes and case classes, respectively), they function similarly regarding readability. Both classes are immutable by default.

  • Lambda Expressions

Kotlin:

val numbers = listOf(1, 2, 3, 4, 5)
val doubled = numbers.map { it * 2 }
val filtered = numbers.filter { it < 5 }.sortedBy { it }.map { it.toString() }.map { it.uppercase() }

Scala:

In Kotlin, you can use inline keyword before a function to suggest the compiler to inline the function at the call site. This means the compiler will insert the function’s code directly into the calling code, eliminating the overhead associated with invoking the function as a separate entity.

While Kotlin directly offers the inline keyword to force inlining, Scala uses the `@inline` annotation to suggest inlining, leaving the decision to the JVM. This can result in less predictable performance improvements in Scala compared to Kotlin’s inline functions.

Kotlin:

inline fun performOperation(sum: (Int, Int) -> Int) {
println("Performing operation...")
val result = sum(5, 3)
println("Result is $result")
}

fun main() {
performOperation { a, b -> a + b }
}

Scala:

  • Pattern matching

Kotlin does not have direct support for match operator. However, it offers a very powerful operator whensupport expression and condition checks.

  • Match on values:
    Kotlin:
when (obj) {
1 -> println("1")
2, 3 -> println("2 or 3")
in 4..10 -> println("between 4 and 10")
else -> println("Unknown")
}

Scala:

  • Match on types:

Kotlin:

when (obj) {
is String -> println("String")
is Integer -> println("Integer")
else -> println("Unknown")
}

Scala:

  • Match with conditions

Kotlin:

data class Person(name: String, age: Int, city: String)

when {
person.name == "John" && person.city == "Bangkok" -> println("Hello John from Bangkok")
person.city == "New York" -> println("Hello Someone from New York")
else -> println("Unknown")
}

Scala with unapply and guards:

2. Kotlins Null-safety Features

One of the standout features of Kotlin is its built-in null-safety support. The type system distinguishes between nullable and non-nullable types, making it difficult to cause a NullPointerException (NPE) accidentally. Kotlin’s type system is designed to eliminate null reference errors by requiring developers to specify if a variable can hold a null value explicitly. This is done using the nullable type syntax, which adds a ‘?’ after the type.

  • Nullability

Kotlin:
val nonNullString: String = “Hello, Kotlin!”
val nullableString: String? = null

Scala: val optionalString: Option[String] = Some(“Hello, Scala!”)
However, in Scala, it is possible to set null by mistake:
val nonNullString: String = null // oops :(

  • Safe calls

Kotlin:


val nullableString: String? = null
val length: Int? = nullableString?.length

Scala:

  • Elvis operator

Kotlin:


val nullableString: String? = null
val length: Int = nullableString?.length ?: 0

Scala:

  • Safe casts

Kotlin:


val nullableString: String? = null
val length: Int? = nullableString as? Int

Scala:

  • Not-null assertion

Not-null assertions should be used with caution, as they effectively bypass the null-safety checks built into the type systems of Kotlin and Scala.

Kotlin:


val nullableString: String? = null
val length: Int = nullableString!!.length

Scala:

3. Leverage Extension Functions

Kotlin allows you to add new functionality to existing classes without inheriting from them using extension functions. This can be particularly helpful in organizing utility methods for specific purposes, ensuring better separation of concerns and cleaner code.

Kotlin:

fun String.isEmail(): Boolean {
return this.contains("@")
}

Scala:

Kotlin’s approach to extension functions is simple, focusing on syntax designed explicitly for defining extension functions. It provides a clean and straightforward way to extend existing classes without modifying the original code.

Scala’s implicit classes, on the other hand, have a broader scope. While they can be used to create extension functions, they also play a part in the more extensive implicit mechanism, which can handle conversions, provide default values, and create type class instances. This versatility can make Scala’s implicit mechanism more complicated and harder to understand for some developers.

Implicit conversions in Scala can accidentally introduce issues when multiple conversions are possible, leading to ambiguity. Kotlin avoids this problem by not providing implicit conversions and focusing solely on extension functions.

4. Collections

Collections and functional programming play a significant role in modern programming languages like Kotlin and Scala. Both languages provide powerful collection libraries and built-in support for functional programming concepts. Here are more details about collections and functional programming in Kotlin and Scala.

Kotlin provides a rich set of collection classes built into its standard library. There are two main types of collections: read-only (immutable) and mutable.

  1. Read-only (immutable) collections: List, Set, Map.
  2. Mutable collections: MutableList, MutableSet, MutableMap. Kotlin also offers a wide range of higher-order functions to perform operations on collections like map, filter, reduce, groupBy, and more.
val numbers = listOf(1, 2, 3, 4, 5)

val evenNumbers = numbers.filter { it % 2 == 0 } // Output: [2, 4]
val doubledNumbers = numbers.map { it * 2 } // Output: [2, 4, 6, 8, 10]
val sum = numbers.reduce { acc, n -> acc + n } // Output: 15

Scala also provides a rich set of collection classes built into its standard library. There are two main types of collections: immutable and mutable.

  1. Immutable collections: List, Set, Map.
  2. Mutable collections: ListBuffer, ArrayBuffer, HashSet, HashMap.

5. Enums and Sealed Classes

enum class TrafficLightColor(val color: String) {
RED("red"),
YELLOW("yellow"),
GREEN("green");

fun description(): String {
return "The traffic light is $color"
}

val redLight = TrafficLightColor.RED
println(redLight.description()) // Output: The traffic light is red
}

In contrast to Kotlin enums, Scala enums are not first-class citizens. They are not types but rather values. This means they cannot be used as types for variables, parameters, or return types. To overcome this limitation, Scala provides a special type called Enumeration that can be used to define enums.

There are a few major issues with Scala enumeration:

  • all enumerations have the same type after erasure. It will not allow using overloaded methods, even with different enumerations as arguments.
  • The Scala compiler does not perform an exhaustiveness check for case matches.
  • Providing an argument that is not covered by the case match, we’ll get a scala.MatchError

However, these limitations were finally resolved in Scala 3 with natively supported enums.

Kotlin’s sealed classes offer an alternative to traditional enum types, providing more flexibility when handling complex state hierarchies. Sealed class instances can have different properties and behavior, which simplifies the code and minimizes the chance of errors.

Kotlin:

 sealed class Color {
object Red : Color()
object Green : Color()
object Blue : Color()
}

Scala:

However, they come with a few limitations:

  • There is no default option to list all values of a sealed class
  • There is no ordering
  • It is not easy to deserialize class from value

6. Concurrency

Kotlin’s primary concurrency model is based on coroutines. Coroutines provide a lightweight and efficient way to write asynchronous and non-blocking code. They enable writing concurrent code in a more readable and simplified manner, using suspend functions and async/await constructs.

Coroutine scopes, context, and dispatchers manage the execution of coroutines, allowing fine-grained control over their lifecycle and execution properties like thread affinity and error handling. Other coroutine libraries, such as kotlinx.coroutines, enhance the functionality with channels, flows, and actors.

import kotlinx.coroutines.*

suspend fun fetchData(): String {
delay(1000) // Simulate an asynchronous operation
return "Data from Kotlin"
}

suspend fun main() {
val data = fetchData()
println(data) // Output: Data from Kotlin

Scala’s Future provides a non-blocking, composable way to represent values computed asynchronously. They are used alongside ExecutionContext to perform async operations and can be combined, transformed, and composed using various higher-order functions.

While both Kotlin’s coroutines and Scala’s Future provide a way to perform asynchronous operations, there are some kotlin coroutines benefits:

  • Unified interface: Suspend functions allow you to represent both synchronous and asynchronous operations by using a consistent API. This unified interface can simplify the code and make it easier to reason.
  • Code readability: Suspend functions enable you to write asynchronous code that looks like synchronous code using suspend, async/await and other coroutine constructs. This approach can improve the readability of your code and make it easier to follow the control flow.
  • Easy composition: Suspend functions make it simple to compose multiple asynchronous operations. You can chain and sequence them effortlessly without resorting to nested callbacks or combining separate techniques like Future and Try.
  • Error handling: With suspend functions, using CoroutineExceptionHandler and structured concurrency, you can better handle exceptions in synchronous and asynchronous operations. This can simplify error handling across multiple operations.
  • Kotlin’s coroutines are lightweight and perform asynchronous operations without blocking a thread. Scala’s Future, however, is a heavier abstraction that requires a thread to run.
  • Kotlin’s coroutines are cancellable. Scala’s Future, on the other hand, is not cancellable and will continue to run until it completes or fails.

7. Kotlin DSL

Kotlin DSLs are a powerful tool for creating internal and external DSLs. They allow you to write code that looks like a domain-specific language, making it easier to understand and maintain. Kotlin DSLs can be used to create internal DSLs for your application or external DSLs for your users.

class ApplicationServer {
val http = Http()

class Http {
var port: Int = 8080
var protocol: String = "http"
}
}

fun applicationServer(block: ApplicationServer.() -> Unit): ApplicationServer {
val server = ApplicationServer()
server.block()
return server
}

// DSL usage
val myServer = applicationServer {
http {
port = 8080
protocol = "http"
}
}

fun main() {
println("Protocol: ${myServer.http.protocol}, Port: ${myServer.http.port}")
}

This example shows how to create a DSL for configuring an application server. In Scala, Typesafe Config library is used for this purpose. It is a library for loading configuration files and reading them in typesafe manner. It supports several file formats, including JSON, YAML, and Java properties files.

However, it is also possible to use DSL

case class ApplicationServer(http: Http)

case class Http(port: Int, protocol: String)

object ApplicationServer {
def apply(block: ApplicationServer => Unit): ApplicationServer = {
val server = ApplicationServer(Http(8080, "http"))
block(server)
server
}
}

object Main {
def main(args: Array[String]): Unit = {
val server = ApplicationServer { app =>
app.http = Http(
port = 8080,
protocol = "http"
)
}

println(s"Protocol: ${server.http.protocol}, Port: ${server.http.port}")
}
}

Kotlin code is more readable and concise. With DSLs, you can write code that looks like a domain-specific language, making it easier to understand and maintain. Having a clear DSL structure, it is possible to avoid using Typesafe configurations in many cases and provide an explicit configuration in code.

Typesafe configuration is difficult to maintain as developers should rely on documentation, and IDE will not hint about possible configuration options. However, mixing both approaches and using Typesafe configuration for default values and DSL for explicit configuration is possible.

Build Tools

SBT is a primary build tool for Scala. It is a powerful build tool that supports incremental builds, parallel execution, and dependency management. It also provides a Scala DSL for writing build scripts.

If you want to support kotlin in scala projects, you can use kotlin plugin for sbt: https://github.com/flock-community/kotlin-plugin.

Gradle is a powerful and flexible build tool that supports Kotlin projects, along with Java, Android, Groovy, and other JVM languages. By integrating the Kotlin programming language, Gradle offers several advantages and features to streamline the building process for Kotlin projects.

Building Scala projects with Gradle is also possible using the official Scala plugin.

That means you can use both languages in one project and smoothly migrate from Scala to Kotlin.

Interop

Kotlin core has 100% interop with Java language. While Scala requires Scala runtime, it leads to issues when Scala and Kotlin languages or libraries are used in one project. But Kotlin extensions also require Kotlin runtime. It became a conversion issue when developers needed to convert `coroutine` into `Future` and visa-versa.

import kotlinx.coroutines.suspendCancellableCoroutine
import scala.concurrent.Await
import scala.concurrent.Future
import scala.concurrent.duration.Duration
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException

suspend fun <T> Future<T>.await(): T = suspendCancellableCoroutine { cont ->
cont.invokeOnCancellation {
this.cancel()
}
onComplete({
try {
cont.resume(it.get())
} catch (ex: Exception) {
cont.resumeWithException(ex)
}
}, scala.concurrent.ExecutionContext.global()) // use right ec instead global
}

and synchronize future with coroutine:

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import scala.concurrent._
import scala.concurrent.duration._

val future: Future<T> = Future {
// Your Scala Future logic here
}

val result: T = runBlocking(Dispatchers.IO) {
future.await()
}

println("Result: $result")

Conclusion

In conclusion, Kotlin and Scala are both powerful programming languages enriching the JVM ecosystem with modern features and elegant expressiveness.

Remember that the decision to adopt Kotlin in a project, especially alongside Scala, should be based on the project’s specific requirements and the organization’s long-term plans. Keep in mind that choosing a single language for a project, if possible, can minimize the complexity and compatibility issues. However, if having both languages is unavoidable, maintain a modular structure to prevent potential problems.

--

--

Agoda Engineering
Agoda Engineering & Design

Learn more about how we build products at Agoda and what is being done under the hood to provide users with a seamless experience at agoda.com.