Kotlin isn’t Java
Kotlin is a very nice programming language that is very easy to learn for teams that already have experience working with Java and add many interesting features that can make the development of your project easier and safer. Indeed, in my personal opinion, Kotlin feels like a sweet spot between Java and Scala, where Java, especially if your team is stuck in older versions, is plain, verbose, and with an important lack of features present in multiple other modern languages.
Scala is a fully-featured language with a crazy amount of features, but with a more complicated learning curve and sometimes compatibility issues with Java. JetBrains fixes these two main Scala problems by providing a simpler, imperative, fully Java-compatible language, but still with a lot of benefits for daily development.
This is probably why you have considered giving Kotlin a chance in your next project at work. If your team is working with Java, or they haven’t ever tried Kotlin, that next new project can be a good opportunity for the whole team to learn and play with something new and experience the upsides and downsides of that decision.
However, there’s a catch: Kotlin is not Java. This can sound pretty obvious, but it is something important to note. If your team starts to develop Kotlin (or Scala) code like they were in Java, you’ll end up with a code base that won’t benefit from the language’s tools.
That is why it is important that, during the scoping of the project, your team can dedicate a couple of hours, or days, to investigate Kotlin, its features, interesting libraries, patterns, etc., so you can decide how your code is going to look like, and what quality standards your team will apply.
However, during these conversations, you need also take into consideration that probably the project will be scoped for a certain time or that your team might not be experienced with Kotlin. This means that the standards your team sets need to balance quality, time consumption, and learning curve so the project can succeed.
During the rest of the article, I will describe the guidelines that I feel are a good idea to consider adding to the coding standards of your next Kotlin project or at least to take into consideration. Note that a few of these guidelines are not Kotlin-specific and can be applied to any compiled language.
Let the Compiler Prove Your Code
This is not a guideline or a specific code standard, but instead, in my opinion, one of the most important tenets that should guide your decision-making.
This tenet, which can be applied to any compiled language, basically means you should try to use all the language features that the compiler offers you to make sure that your code is correctly written, and if not, instead of crashing in runtime, make the compilation fail. This principle is fundamental for eagerly catching errors in compile time rather than in runtime, where they can negatively impact customers or be more expensive to resolve if these errors reach a production environment.
Kotlin has multiple semantics that can help you achieve this, including the type system, nullability checks, experimental contracts, sealed classes, etc. We will discuss how we can benefit from these tools more concretely in the following paragraphs.
Type system proofs
Before moving on, we should probably take a few minutes to discuss the importance of types. When I’m asked to take an already existing JavaScript code that I haven’t written by myself, I have a bad time figuring out how everything works because, for every single variable or field that is defined in the project, its value can be literally anything, as there’s no way to limit what values can fit into a variable.
Types in a language, expressed in set theory concepts, allow us to limit the domain of the possible values that can be stored in a variable, parameter, etc., and potentially also express properties of the legal values inside that domain. And this is an extremely powerful tool that can help us to prove (at least informally) some properties of our code that can help us to prevent unexpected bugs by failing the compilation.
Let’s write an example that probably it is too complex to be used in a real production environment, or at least it is not common to see in real-world code in Kotlin. Suppose we have a function that multiplies the first element of a list by the sum of the rest (tail) of the elements of the list. A first approach for doing this would be something like:
fun multiplyTail(list: List<Int>): Int =
list.first() * list.drop(1).sum()
This function takes as a parameter a list of integers and returns a single integer. In other words, its domain is all the possible list of integers of any length, and its codomain is any integer. So, this function will basically map every single possible list of integers to a value of type Int… except for one specific case. If an empty list is given, then the function will throw an exception instead of returning normally an integer: our function is not defined for the empty list case.
To prevent the function’s domain from being wider than the domain it is defined in and prevent it from throwing exceptions inside the function, we can use types to prove that the passed list always has at least one element. It could look like something like this:
@JvmInline
value class NonEmptyList<T> private constructor(
private val list: List<T>
): List<T> by list {
companion object {
fun <T> of(firstElement: T, vararg tail: T): NonEmptyList<T> =
NonEmptyList(listOf(firstElement) + tail)
fun <T> fromList(other: List<T>): List<T> {
require(other.isNotEmpty(), { "List cannot be empty" })
return NonEmptyList(other)
}
}
}
fun multiplyTail(list: NonEmptyList<Int>): Int =
list.first() * list.drop(1).sum()
fun main() {
println("Result: ${multiplyTail(NonEmptyList.of(1, 2, 3))}") // This works
println("Result: ${multiplyTail(NonEmptyList.of())}") // This doesn't compile
println("Result: ${multiplyTail(NonEmptyList.fromList(listOf()))}") // This compiles, but fails in runtime.
}
In this example, we’ve created a type, NonEmptyList<T>, that cannot be constructed unless the constraint of having a non-empty list is satisfied (well, it can maybe if you start playing with hacky reflection, but not if we strictly respect the visibility rules of the language). By doing this, we’re proving, again, informally, that having a NonEmptyList instance implies that the inner list is not empty. In the Scala world, this could be referred to as a refined type. Even though this is not a common pattern in Kotlin, we’ll see later in this article that they are still quite useful under some circumstances.
Another example of proofs we can do with types is by using the type Nothing. This type represents the bottom type in Kotlin and is inhabited, meaning its domain is empty. What this means in terms of proofs is that, the only way to have a value of type Nothing at a certain point in code, given the fact the domain of the type is empty, is because the code is unreachable or, in the context of a function returning this type, that it cannot normally return except by throwing an exception. For example, take a look at these three examples:
fun thisFunctionCannotBeEverCalled(value: Nothing) {
}
fun infiniteLoop(): Nothing {
while (true) {}
}
fun raiseListEmptyException(): Nothing {
throw IllegalArgumentException("List cannot be empty")
}
By using the type Nothing, we’re proving different statements:
- In the first example, we’re proving that since there’s no value that can satisfy the input parameters, that function cannot be ever called.
- In the second and third examples, we’re proving that the function will never return normally. This is especially useful for letting the compiler know that some of our code branches are unreachable so that it can compile our code based on that invariant. For example, in Java, if you want to exit from a CLI application eagerly, you’ll do something like:
public static void main(String[] args) {
String firstArg;
if (args.length == 0) {
System.err.println("Invalid args count");
System.exit(1);
return;
} else {
firstArg = args[0];
}
System.out.println(firstArg);
}
Clearly, the return statement on the if guard is unnecessary because after the call to exit(1), the process will always stop its execution, and that call will never return. However, if we don’t write it, the program will fail to compile since Java cannot guarantee that the variable, firstArg, hasn’t been initialized before its first use; it cannot infer that the first branch of the if won’t ever return. In Kotlin, the function, exitProcess, is defined by returning a Nothing as result for that reason. Here’s what that looks like:
fun main(args: Array<String>) {
val firstArg: String
if (args.isEmpty()) {
println("Invalid args count")
exitProcess(1)
} else {
firstArg = args.first()
}
println(firstArg);
}
Conversely, in this case, the code compiles without any problems because, thanks to the Nothing type returned by the exitProcess function, Kotlin can infer that the first branch of the if won’t ever return, and therefore if the code reaches the last println, is because the code flowed through the else branch of the condition.
Forbid the Usage of The!! Operator From Day 1
This is probably one of the most important concrete guidelines to consider applying to Kotlin, and, indeed, Detekt has some rules active by default for catching this.
One of the most common runtime errors we find in Java is the well-known NullPointerException, and it is produced by the fact that the type system treats as valid the value “null” as a part of the domain of any non-primitive type. This is from the very foundations, something that needs a rethink: a type system that allows having a value that means “absence of value” inside the domain of a type is problematic because there will always be one case that you need to treat specially for every single non-primitive value in your code.
It gets even worse when this special case is transparent to the developer and the compiler, where any variable or parameter can implicitly hold this value without having effective methods to differentiate variables that may hold null values from the ones that cannot (There’re ways, like using the @NotNull annotation, but it is not frequent to see it in real code, and the majority of the time, it will generate warnings on compile-time and not hard errors.
Conversely, Kotlin handles this specific case by allowing to specify a marker (?) that differentiates a type that can contain a nullable value from a type that cannot. This solution is similar, but not strictly the same, to those languages, like Rust or Scala, that represents the absence of a value with a “wrapper type” that has two possible variants: One where the value is present and provides access to that value; and another when there’s just no value. Either way, these approaches allow you to handle those situations where the value is proven to be always present differently than the ones where the value might not be there. In the latter case, this will force the program to always handle the null case, thus, preventing any NullPointerException.
However, there’s still two main possible situations in which Kotlin can still raise a NullPointerException:
The first one is when calling Java code with null values or using objects returned by Java code: In these cases, Kotlin cannot be sure whether the objects should be null when calling Java code or the returned values from Java code can be null unless they are explictly marked with the @Nullable or @NotNull annotations. In these cases, it is up to the developer to enforce or not rules to always check the nullability of the returned values for preventing further errors in the Kotlin code.
The second one is when using the !! operator: This operator allows accessing values of types marked as nullable directly without any extra checks. Therefore, it opens the possibility of NullPointerExceptions to be raised. And this is the reason why it shouldn’t be used. Abusing this operator will lead our Kotlin code to have no benefit; it could have been written in Java, in terms of nullability issues.
When we are in a situation where we find the !! operator useful, it is typically when we want to access a possibly null value we “know” won’t ever be null. This happens when we, as developers, are unable to find a way to prove to the compiler that the value won’t ever be null. And this statement can be wrong because of a bug that doesn’t produce a compilation error and gets hidden. Maybe it is not a problem today, but it can become a problem in the future — when the code becomes bigger, has logic changes, or the project’s ownership shifts to people with no context about the code, etc.
Generally, the main solution needed to prevent using this operator is to refactor our code to find ways to prove this constraint to the compiler. Here we have some ideas about how to fix some scenarios I feel are common when developers try to use the !! operator:
- Input validations: if your system receives a set of inputs that can be nullable, for example, those represented by a data class where all its fields are marked as nullable, and your system validates the non-nullability of all those fields, make sure you propagate the results of that validation outside your validation functions. You can do this by defining a new class that has the same fields as the original data class, but with all the fields marked as non-nullable. By doing this, you’ll be telling the compiler and the rest of the system that you’ve properly verified that those values are not null, and you can use that constraint across the rest of the system.
More generally, a good practice to better allow the compiler to prove our code is to represent that value with a more precise type that proves that constraint. Of course, this statement needs to be taken with care, as trying to represent the result of every validation, including things like list sizes, integer signs, etc., can be so verbose that they’re not really worth it. An example of this validation could be something like this:
data class CreateUserRequest(
val name: String?,
val email: String?
)
data class ValidatedCreateUserRequest(
val name: String,
val email: String
)
fun validateCreateUserInput(input: CreateUserRequest): ValidatedCreateUserRequest {
if (input.name == null) {
throw BadRequestException("Name cannot be null")
}
if (input.email == null) {
throw BadRequestException("Email cannot be null")
}
return ValidatedCreateUserRequest(name, email)
}
- A field in my model will always be not null only if another field in my model is not null as well: In this case, a good recommendation will be to wrap these two fields into another model that can be itself nullable so that the nullability constraint of those fields are linked together. For example:
// Instead of this
data class Person(
val name: String,
val tshirtType: TShirtType?,
val tshirtColor: Color?
)
// We can rearrange our model like this
data class PersonTShirt(
val tshirtType: TShirtType,
val tshirtColor: Color
)
data class Person(
val name: String,
val tshirt: PersonTShirt?
)
Make Invalid States Unrepresentable
This principle guides you to think in your data model in such a way that its types can represent as accurately as possible (and up to a reasonable point) the domain of all of the possible values that are legal by your own business rules so that we can prevent extra validations or unexpected conditions in the middle of our business logic. Kotlin has different tools that we can use for this, such as the nullability marker that we were previously discussing, enumerations, sealed classes, etc. We can also apply some other techniques, like the definition of refined types.
As an example, let’s suppose that our application models employees in different positions within a company, and each different position has some attributes associated:
data class Employee(
val name: String?,
val email: String?,
val position: String,
val directReports: List<Employee>?
)
In this example, we can define that the only valid positions are SALES_REP and SALES_MANAGER and only the latter position can have reports.
With this data, there’re some improvements that we can do on our model to make some invalid values unrepresentable:
- First, in this case, we can suppose that name and email are mandatory because there can only be an employee with those attributes. We can remove the nullability marker then.
- Secondly, the position is very loosely defined. The position could be defined more precisely with an enumeration or a sealed class to limit the number of possible values.
- Third, since only a sales manager can have reported, then we can rearrange our model to include the directReports field in a sealed class that depends on the position, so we can only access this value only when we’ve matched the position, which will allow us to remove the nullability marker as well.
By applying these suggestions, our final model will look like this:
sealed class EmployeePosition {
object SalesRep : EmployeePosition()
data class SalesManager(val reports: List<Employee>): EmployeePosition
}
data class Employee(
val name: String?,
val email: String?,
val position: EmployeePosition
)
Refined types
In our example above, we’ve defined the email of an employee as a string. But doing so has some problems:
- Firstly, it is a very vague definition. Because in the infinite domain of all possible strings of any length, the majority are not valid emails. That means, that, in the event of a bug, we could have invalid emails stored in that field, or passed across functions that do need a valid email to properly do their job.
- Secondly, in a situation where a function takes multiple string parameters, it is quite easy to commit a mistake and, for example, swap two string parameters and end up with an incorrect function call.
One pattern we can apply here is to use something that is pretty common in the Scala world, which are refined types. These types are just “wrappers,” or maybe just markers, depending on how we implement it, around other types, that limits the domain of possible values of the original type. For example, we can have a refined typed that limits an Int to have a positive value or a string limited by a regular expression. This allows us to be more precise with our types. We can consider our type NonEmptyList from one of our previous examples as a refined type: a list that satisfies that at least holds one element.
In Kotlin, I don’t know any established library for automating the definition of refined types, but the truth is that they are quite easy to define anyway. For example, in Kotlin, we can define a refined type that represents an email by something like this:
@JvmInline
value class Email private constructor(val value: String) {
companion object {
private val EMAIL_PATTERN = // ...
fun of(value: String): Email {
if (EMAIL_PATTERN.matchEntire(value) == null) {
throw IllegalArgumentException("Value $value is not a valid email because it doesn't match pattern ${EMAIL_PATTERN}")
}
return Email(value)
}
}
}
Note that we’re using an inline value class to reduce the performance impact of creating a wrapper around our value.
Of course, what these refined types are doing is reducing the size of the domain of a type by, in the case of the example, using regular expressions for matching valid emails. But that doesn’t mean that the emails are valid: it just means that we’re reducing the list of possible invalid emails so we can represent our data more accurately, but it doesn’t have anything to do with any further check that we can do for ensuring that the email exists, or that it uses registered domains, etc.
Reasonable limits
Okay, the idea of making invalid states unrepresentable sounds nice, but there has to be a limit. Depending on your use case, it could be extremely hard to achieve a perfect match between the domain of your data and your business rules. For example, maybe you’re not interested in refining every single number in your application to prove they’re positive, negative, or between two numbers (especially this latter one which can be hard as Kotlin doesn’t support something like const generics). Maybe in those situations, you just prefer to fall back into the classic previous validation of parameters at the beginning of a function and call it a day.
Thinking about how to model your data to match this guideline, or even dealing with a very complex data model just because of it, could also be problematic because of all the extra costs of maintaining that code and developing more on top of it. There has to be a balance between code quality, maintainability, and development speed, always.
Mutability Is the Root of All Evil… Mostly
Mutability is something that generally must be handled with care. Having too much mutability makes your code more complicated to follow, harder to maintain, and harder to parallelize. Unless you’re building the next generation of database engines and you’re building something more similar to a web server or APIs instead, probably it’s easier for you to keep mutability at a minimum.
This includes not only the state of classes and applications in general but also at the function level. Especially at the function level, developing algorithms and procedures that make use of immutable data structures usually helps to end up with clearer code. There are exceptions, of course, like if you’re working with circular data structures like graphs, or you just have an algorithm that you are not able to generalize into more abstract operations, and you just prefer to use mutability for coding it.
This is why it is a good practice to, while working with collections, try to reduce or eliminate the use of fors in your algorithms and try to use the more abstract methods that the Kotlin Standard Library has defined for working with collections. I’d recommend taking a second to read the official Kotlin documentation and refresh how many things you can do with the Kotlin collections API.
Handling Error Handling
In Java, the most common way to handle errors is just with exceptions: You just fire exceptions whenever you need, and of the type you need, let the callers handle them, and that’s pretty much it. In Kotlin, I’ve experienced that is a little bit harder than that. But let’s start from the beginning.
Java exceptions were “nice” before Java 8: If you have a custom method that raises custom exceptions, you add those exceptions to the “throws” clause in the method signature, so you can inform the caller of that function that it could fail for a very specific set of issues, and it forces you to handle them (checked exceptions). This not only provides you with documentation burnt into the signature method about which ways a function can fail, but it also forces you to handle each case or delegate recursively to your caller, so it can handle them.
The problem comes, however, with the introduction of Lambda Functions in Java 8. And this is where you start to experience that it is hard to combine exceptions with them because functions that define thrown exceptions are very hard to compose together. And you could end up having a situation like this:
public String processElement(String value) throws SomethingWentBadException {
// ...
}
public List<String> processList(List<String> input) {
return input.stream()
.map(this::processFilter) // This fails to compile!
.collect(Collectors.toList());
}
This is a problem! Because the filter function doesn’t expect the passed Lambda to throw anything, but in our case, it does, and it fails to compile. So either you do this:
public List<String> processList(List<String> input) {
return input.stream().filter((e) -> {
try {
return processFilter(e);
} catch (SomethingWentBadException ex) {
logger.error("Error!", ex);
}
}).collect(Collectors.toList());
}
… which is horrible, not only in terms of code style but also because of the fact that you’re suppressing exceptions to be thrown as expected. Or you use the classic Lombok’s @SneakyThrows, which is also opinionated because it will prevent you from handling this exception if you need it in another part of your code. It will also hide the exception from the method signature. The last option could also be to define your own map function that supports exception throwing.
In Kotlin, this doesn’t happen because the compiler doesn’t enforce adding the exceptions to the method signature, and all of the exceptions work as if they are RuntimeExceptions. So the code above will compile properly in Kotlin.
However, this also has some downsides, and you probably noticed what they could be: If there’s no checked exceptions, then we don’t know what and how a method can fail, which makes error handling more obscure and less explicit than it used to be with Java.
Another interesting approach would be to use the Either type of the well-known Arrow Library. This library, among many other things, has defined the type Either<L, R>, which definition can be simplified as follows:
sealed class Either<out L, out R> {
data class Left<L>(val value: L): Either<L, Nothing>
data class Right<R>(val value: R): Either<Nothing, R>
}
Basically, the Either type allows us to hold either a value of type L or a value of type R. In the case of error handling, we can use this to store a value on the right when everything went OK and a value on the left if something bad happened. This will let us store the reason for the error there (Storing the “right” value in the right and the error in the left is just a convention, but it is convenient to follow it because the Arrow API has some useful tools based on this arrangement). How we store the errors on this left side is up to the developer, and they can be any exceptions nested into the Either type or sealed classes that perfectly describe all the possible errors the function can raise so they can be pattern-matched by the callers.
Using the Either type means we can recover the visibility of the errors that we’ve lost with the removal of checked exceptions in Kotlin and being forced to handle the errors again but in a more composable style.
However, this approach is not exempt from downsides, and there are a few to take into consideration:
- It is more verbose than normal exceptions and sometimes harder to learn. If your team has ever worked with this before, it could be something that can make harder the learning curve.
- Calling Java code, and being called by Java code or by code that uses exception for error handling, becomes harder: Every time you need to use a code that can throw exceptions, you probably need to wrap those calls into functions that can handle exceptions that happen internally and map them to a proper Either instance so you can propagate that error through your Either-based exception handling application. This adds an extra step every time you want to call these kinds of APIs from your application.
- You’ll need to deal with exceptions anyway: This is not necessarily a downside, but just something to consider. All the exceptions cannot just be replaced by Either-style error handling, basically because there are exceptions that are fired through the Kotlin Standard Library that you cannot change. But this is also how error handling works in Rust, for example: For errors produced by I/O errors, system errors, etc., a Result (the equivalent to an Either in Rust) is used to propagate the error. But if a fatal error happens, like an attempt to access an index out of the bounds of a list, a panic is thrown, which would be equivalent to our exceptions. In this way of handling errors, fatal errors are still throwing exceptions, but other errors that can be recoverable, such as network errors, validation errors, etc., can be propagated with the Either monad.
So, choosing between one mechanism or another depends on the expertise of your team managing this style of coding, how much you depend on Java code, your library dependencies, how you are planning to perform error reporting and error handling, etc.
Finally, after explaining how we would do error handling using Arrow in Kotlin, we can finish our example:
fun processElement(value: String): Either<SomethingWentWrongException, String> {
// ...
}
fun processList(input: List<String>): Either<SomethingWentWrongException, List<String>> {
return input.traverse(::processElement)
}
Coroutines or Not Coroutines?
This is also a very interesting topic to think about when starting a new project, and a decision that should be taken as soon as possible in a project because of the red and blue functions problem. If you’re starting a new web server, you might think that it is a good idea to use coroutines. But there’re some points to take into consideration:
- Are you depending on libraries that strongly depend on the thread-local variables? If that’s the case, you should take some minutes to investigate how much effort it can take you to create the Coroutine Context Elements that you’ll require to make that library compatible with Coroutines.
- Are you heavily depending on dependencies that use blocking I/O? If such is the case, I still see benefits in using Coroutines because they have an extensive API that can still help you to parallelize downstream requests, create timers, cancellable requests, etc. But it is imperative to take into account that coroutine threads must not be ever blocked by long-running processes, including calls that evolve expensive CPU computations or blocking I/O calls. And that’s a problem because there’s no accurate linter that can warn the developer that they’re blocking calls from a coroutine thread. This is something to consider as it is another source of possible bugs that can easily trash the performance of your application.
But what if you are developing, for example, a short-lived AWS Lambda function or a CLI application? Does it make sense to have coroutines? Well, for me, it made sense because, in different parts of my application, I needed to do parallel queries to my downstream services. Coroutines let me build the application more generically and simply, so I could use them when needed. If you feel this might never be your case, it probably doesn’t make sense to go that route.
Conclusion
This is just a glance at some aspects I needed to make a decision in my last Kotlin project, and it considers the timing, team experience, and code quality. It is not an easy thing to do, and it depends on your resources and the dependency of this project with other already existing legacy Java projects, and other factors. You may want to go in a different direction. Regardless of what standards you choose for your next project, just remember that Kotlin is not Java.
Establishing the Standards of a Real-World Kotlin Project was originally published in Better Programming on Medium, where people are continuing the conversation by highlighting and responding to this story.