Before we dive into the Kotlin language, I would like to take some time to explain what drove me to write the Primer in the first place. After all, Kotlin comes equipped with fairly extensive documentation, and the internet is chock-full of material, so why bother writing yet-another-Kotlin-resource? This article will attempt to answer that question, explain (one of) the objectives that guide the way the material is presented - maintainability - and explain why I chose this specific objective.
From time to time in this article, I will be referencing certain features of Kotlin, which, if you are a Kotlin freshman, you will not understand! So let me put you at ease right away — there is no need to understand what they are. Feel free to ignore them and keep reading, they’re only there for context and aren’t important to understand the rest of the text.
How the Primer came to be
It has been my experience that moving from Java to Kotlin presents a few challenges. Chief among those is the fact that, while the languages are similar in many aspects, Kotlin introduces a whole host of features whose main benefits are technically not in function (i.e. what can be achieved), but in form (how these things are achieved).
When I started to learn Kotlin, I went through a confusing period in which not only did I not understand how to use such features deliberately and consistently, but I did not even understand that there was something to understand. Coming from languages such as Java and (long ago) C++, I was guided purely by function, and could not conceive that there was more that one could strive for. Indeed, if you have no experience in Kotlin, you might have no idea what the heck I’m even talking about!
It was only through experience (of which there has not been much!), gradual experimentation, and inspiration from many online resources and various other languages, that I discovered there were indeed principles I could adhere to that helped me produce code that seemed subjectively better, and I would even go as far as to say objectively better as well. You’ll see examples of what I mean throughout the Primer.
However, having combed through countless Medium articles, tutorials, and other learning resources, I often got the impression that discussions about form — when and in what manner to use a certain feature — took second place to discussions about function and mechanics, i.e. what it does and how it works. When I did encounter a discussion about form (e.g. when to use an extension function vs. a member function or when to use certain scope functions), more often than not, I found it either did not answer the questions I had, or answered them in a way that seemed prescribed and lacked structure or consistency.
Most frustratingly, the question of why a particular choice should be made was often left unanswered. Of course, this is not true universally, but, by and large, my impression was that this subject was given less attention than I felt it should.
This is one of the main reasons I decided to write the Kotlin Primer, and if I had to pick a single feature that sets it apart, it would be that it attempts to address these questions explicitly.
It is important to understand that the material in the Primer is motivated in a large part by my own experience and tailored to my own personal values, which means it is, by necessity, heavily subjective — the Primer is an opinionated guide to the Kotlin language. To draw a comparison to the literary world, the function and mechanics of Kotlin features are akin to vocabulary and grammar — they are objective things, and are explained in a dictionary, thesaurus, or grammar reference. But you will not learn to write a novel from a dictionary. For that, you need to learn a certain style of writing, and that is, by necessity, a subjective endeavor.
In this sense, the Primer could be thought of as a resource for learning a certain style of prose — it will teach you the vocabulary and grammar, but also how to use them to emulate a certain style and achieve a certain result. And while I will always explain why I prefer a certain style or use a certain feature in a particular way, you may find that you disagree with my views, in the same way, that you may or may not find a particular literary style attractive.
But that’s a good thing! I firmly believe that simply being confronted with an opinion, and forming your own, is a beneficial learning experience that makes reading the Primer worthwhile, even if the opinion you end up forming is different from the one that’s presented here.
Think critically about everything you encounter in the Primer, and constructive criticism in the comments is encouraged, welcomed, and appreciated.
On function vs. form
When pondering how to code a solution to a particular problem, things are usually easier when we are guided by function, i.e. what needs to be achieved. Need to iterate a list? Use a for
-cycle. Need to deal with a failure? Throw an exception (or should you?). However, this becomes much more challenging when multiple tools achieve the same goal, differing only in form — how they achieve it. Which feature should we use in a particular situation? More importantly, why? How do we consistently make these choices, and what are the qualities that are influenced by these choices?
In reality, these challenges are not unique to Kotlin. Indeed, it can be argued that in any language, all features beyond those strictly necessary for Turing completeness add nothing but flexibility in form, and Java is no exception. For example: How do you decide if a method should be protected
or private
? How do you decide whether to pass a piece of data into a method directly via a parameter or instead store the data in a field of the enclosing class, which is then accessed inside the method? In the vast majority of situations, it is possible to write code that will perform its function correctly (i.e. produce the correct result) for both of those implementations.
To be able to make these choices consistently, one must make them with purpose — there must be a clear objective, beyond simply writing code that functions.
Interestingly, it has been my experience that many professional developers don’t understand this. They don’t understand that something more than function can (and must!) be taken into consideration, and that the reasons for this are practical, as opposed to being an exercise in rigorousness, artistic indulgence, or philosophical purity. And while determining this objective is, in general, context-dependent, there is a clear choice to be made under reasonable assumptions, which are likely to apply to a large number of organizations.
Those assumptions are:
- A growing code base of one or multiple applications with indefinite lifecycle lengths spanning at least multiple years. In other words, the apps are expected to last.
- The majority of these applications are typical corporate applications, where performance is not a top concern. This does not mean that it’s okay if a page takes 20 seconds to load, it means that we do not have to render a scene in 4K resolution 60 times per second, or process mountains of trading data in real-time. In the corporate applications we’re talking about, performance constraints are measured in seconds or larger.
- These applications evolve organically over time — features are added, modified, or removed — and this evolution cannot be accurately predicted ahead of time. Implementing these features must be done within financial and technical constraints, which are usually proportional to the business value of said features. In other words, you can’t rewrite the whole app whenever something needs to be tweaked.
- The work is done by a team or teams of developers with continuous turnover. In other words, developers are constantly leaving or rotated to other assignments, with new developers being onboarded in their stead.
Given these assumptions, and observing that code can only ever be written, maintained (i.e. read or possibly modified), and executed, it is my firm belief that the majority of the time (and, therefore, cost) is spent maintaining code. Therefore, one of the main objectives of the Primer is to promote maintainability, and introducing Kotlin features from a perspective that focuses not only on writing code that is correct, but also code that is easy to maintain.
The importance of maintainability
Superficial evidence seems to suggest that, for a long time, code was written for computers first — computers stood at the center of design decisions. Code needed to be functional, and ideally, fast. With the dawn of commercial programming, business considerations started to share this pedestal — code had to be functional, ideally fast, but also completed on time and within predetermined cost constraints. Factors such as, but not limited to, maintainability rarely took center stage. These statements seem at least plausible given the prevalence and colloquial meaning of the term legacy code — old code you really, really don’t want to touch, and therefore, maintain.
In an almost amusing twist, it turned out that shunning maintainability ended up negatively impacting the very interests it was shunned in favor of. Code designed and written for computers first is often difficult to maintain, as is code written under pressure from deadlines or available budget, where shortcuts are likely to have been taken and technical debt incurred. Code that is difficult to maintain increases the likelihood that mistakes will be made while reading and/or modifying it. The cost of implementing changes increases. Regressions are introduced. As a consequence, correct function is no longer achieved, user satisfaction is affected, cost constraints are not observed, and business is impacted.
Maintainability is a critical quality of code, especially (but not exclusively) in environments where the previously mentioned assumptions are satisfied. More importantly, maintainability is fundamentally a business concern, which is often still not understood by many stakeholders. When maintainability is not made a first-class citizen, the technical debt grows without bound, and hell ensues.
It would seem that things have changed at least somewhat, and maintainability is getting recognized as the business concern it has always been, even though the degree to which this is understood still varies significantly across organizations. Code is no longer written for computers first, but also with future maintainers — programmers — in mind. Higher-level languages, paradigms such as object-oriented and functional programming, and methodologies such as test-driven-design and agile development all directly or indirectly promote better maintainability, and although many of them arose from an academic setting, it seems ostensible that increasing maintainability is among the reasons these things came to be adopted and continue to be used.
If true, this would imply that maintainability is one of the main forces behind the continuous evolution of the developer world.
Maintainability in the Primer
There are two specific considerations related to maintainability which are emphasized throughout the Primer — communicating intent and maximizing compiler involvement.
Communicating intent
Communicating intent involves writing code in such a way that we communicate to the reader our state of mind, the mental framework we’re using to model the problem, and the approach we’re taking to solve it. With tongue-in-cheek, one might say that it is not unlike writing poetry, where one also encodes additional information — meaning, emotion, and other artistic qualities — within words that do not ordinarily carry it.
Going back to the questions posed at the beginning of the article, communicating intent is precisely what features which differ only in form are good for.
For instance, it could be argued that designating a method public
, protected
, or private
doesn’t make practical sense outside a library setting, since your team has control over the code — there’s nothing stopping anybody from simply changing private
to public
. However, it is communicating intent — methods constituting the public
interface are usually the ones you want to look at to understand what an object is and what an object does, while protected
methods hint at the way the author intended their code to be used to solve similar problems in the future. In other words, proper choices of visibility modifiers can help the future maintainer focus his energy and be more efficient.
From this perspective, Java’s vocabulary is very limited, and communicating intent is not something you think about when writing Java code — indeed, you would probably not think of using public
/protected
/public
as communicating intent but simply as adhering to conventions and writing clean code. However, Kotlin expands your vocabulary dramatically and makes this discussion even more relevant.
The Primer encourages you to take advantage of these features and to think hard about how to write code that communicates its intent clearly. Understanding when to use, and more importantly, when not to use, certain Kotlin features is a hurdle every Kotlin newcomer faces. I have noticed many people that switch from Java to Kotlin undergo a euphoric phase in which new Kotlin features are used wherever possible, simply for their own sake, and to such a degree that it degrades code quality. I jokingly refer to this phase as Kotlin puberty, and there is a KotlinConf talk dedicated to this tendency.
One of the places where intent is discussed most prominently are the chapters on scope functions and extension functions, both of which are features that add nothing but form, and are often struggled with for precisely that reason.
Maximizing compiler involvement
An unfortunate consequence of the assumptions listed earlier is the fact that, in the vast majority of cases, the maintainer can't be aware of the entire code base. This is due to practical issues, such as having just been onboarded, or documentation being out of date (because it always is), but also fundamental issues — above a certain code-base size, it is simply not possible to fit all the necessary information inside the brain of an average human being. And most corporate code bases comfortably exceed this size.
When this limit is reached, mistakes are guaranteed to start happening. You add a child class that extends from a base class but are unaware that there’s a conditional somewhere that branches over inheritors, and you don’t add the corresponding branch for your new implementation. You call a method but are unaware that it throws a runtime exception and forget to map it to a domain-specific one, causing it to fall through existing error handlers. You change the implementation of a method but are unaware that this breaks the assumptions of one of its callers. The list goes on.
It is not a question of if these mistakes will happen, it’s a question of what happens when they do start happening. There are two ways these types of mistakes can manifest themselves, specifically compile-time errors and run-time errors, and it is difficult to overstate how different the two are in terms of negative impact.
Compile-time errors are nothing. They are discovered immediately, independently from the existence of tests, and can never make it into production. If an application builds successfully, it provably does not contain any mistakes from this category.
In contrast, run-time errors cannot be discovered during build time, need tests to be discovered, and can easily make it into production. Crucially, a fully green test suite does not imply the application contains no such mistakes — it is never possible to prove that.
There are two possible manifestations of run-time errors — runtime exceptions, and incorrect calculations. The latter represents the worst-case scenario because it represents a silent failure. It is often not noticed and can lead to disastrous business consequences. The former are usually easier to spot because there is usually some visible indication that something went wrong — an infinite loading page, a red banner, a log entry or monitoring alert, etc. — but the consequences can be just as severe when they’re caught late.
Kotlin introduces features that allow us to transform whole categories of run-time errors into compile-time errors (although, to be very clear, these features did not originate in Kotlin), and this is what I consider to be its most fundamental advantage — Kotlin will help you write safer code. Type-level nullability is the most commonly quoted example of such a feature, but there are many others. Sealed classes and value classes pave the way for strongly typed programming techniques, and the Result
class provides tools to finally dispose of exceptions once and for all. While it might sound too good to be true, whole subcategories of the mistakes listed above (missing switch branches, etc.) can be transformed into compile-time errors.
I hope that this has elucidated some of the reasons why the Primer came to be, and what you can expect from it. If I were to sum this article up in three words, it would be maintainability, maintainability, maintainability.
Without further ado, let’s dive in.