Dissecting Kotlin: Considering Conventions

GitHub: https://github.com/queencodemonkey/dissecting-kotlin-considering-conventions

Introduction

DtsII.kt: L17

Hello, DroidKaigi! I am Huyen Tue Dao. I am an Android Developer for Atlassian working on the Trello Android application.

I have been working in Android for nine years and in Kotlin for three years.

DtsII.kt: L52

In the last three years, I have often asked myself, "What is idiomatic Kotlin?" Now I ask myself this because I have thought that writing better Kotlin means writing more idiomatic Kotlin.

But what does idiomatic mean?

If you look up the meaning of "idiomatic code" you will proably find answers saying that idiomatic code is:

  • "native code"
  • "natural code"
  • "code that follow's the language conventions"

What Is Idiomatic Kotlin?

DtsII.kt: L72

But what is "native" or "natural" Kotlin code? I still find this to be a difficult question to answer.

When I first started learning and using Kotlin, I thought that a good starting point for idiomatic Kotlin was to learn and use as many Kotlin language features as possible.

In fact, in a previous talk that I gave, I very quickly reviewed many of my favorite Kotlin lanuage features and stdlib tools. It's interesting that the format of that talk is a good reflection of how I first tried learning Kotlin: trying to understand and utilize as many new features as I could as quickly as I could.

However, as I have gained more experience with Kotlin, I have unconciously moved away from ths approach. Rather than learning as many features as I can, I like to focus on the philosophy behind Kotlin. I want to understand the goals and values of the team writing Kotlin.

Looking at Kotlin Pragmatically

DtsII.kt: L90

This approach was inspired greatly by the KotlinConf 2018 keynote given by Andrey Breslav. In the keynote Andrey talks about many things, but I became very intrigued by a section of the talk where Andrey talks about the beginning and the goals of Kotlin.

In particular, Andrey says that the team's "main goal" was to create a "pragmatic language."

The team wanted to create a language that helped developers get things done.

In particular, they wanted developers to have the ability of "…turning your thoughts into working software without any hoops to jump through."

DtsII.kt: L112

Now there was one part of the keynote that really suprised me and really made me re-evalute my approach to Kotlin. This part is when Andrey discusses several characteristics for which people laud Kotlin but which are not as important from a pragmatic perspective and then replaces each characteristic with a more pragmatic value.

Readability > Concision

DtsII.kt: L117

First, Andrey notes that many people compliment Kotlin's concision: you can write very concise code. While verbose code and boilerplate can be annoying to manage, hard to to read, and a hiding place for bugs, we should not write concise code solely for the sake of writing concise code. If you have ever heard of the phrase "Code Golf" (where a developer tries to write as concise of code as possible) or have had to review "Code Golf" code, you're probably familiar with the downsides of overly concise code: hard to read and understand, hard to maintain. Of course, the ideal is somewhere in the middle, right? That is why the Kotlin team cares most about readability.

Reuse > Expressiveness

DtsII.kt: L118

Second, Andrey notes that people love the expressiveness of Kotlin: that you can do so many different things in the language. I have to admit that I also talked a lot about the expressiveness of Kotlin too. And while that is a positive trait, Andrey goes on to say that what the team cares more about his reuse: having abstractions that allow developers to take commonly used code and patterns and turn those into libraries.

Interoperatbility > Originality

DtsII.kt: L119

Third is originality: novel features or syntax that are unique to Kotlin. While the originality of some Kotlin features is something that people love, the team doesn't want Kotlin to stand out but instead to blend in. Interoperability has been a huge benefit to the adoption of Kotlin. You might have experienced this in your own teams migrating from Java to Kotlin and the ability to still utilize existing Java code and libraries while writing new features in Kotlin or refactoring to Kotlin. Interoperability allows developers to leverage existing knowledge and tools.

Safety/Tooling > Soundness

DtsII.kt: L120

Soundness is quite a complex and academic topic, but I can offer a poor paraphrase. Soundness is more or less the mathematical correctness of the compiler and assurances that it's always correct about the safety of a program. While it is an interesting perspective and topic, soundness is not the most pragmatic approach. What the team cares more about is the safety that comes from the Kotlin type system and the tooling that the is provided with Kotlin.

Keeping Pragmatism in Mind

DtsII.kt: L124

So keeping these more pragmatic principles of the Kotlin team in mind, let's look at a few features of Kotlin and try to find these pragmatic values reflected in these features.

Conventions

DtsII.kt: L137

The common characteristic among the features that we will discuss is the fact that they all are based on the Kotlin concept of "conventions." In Kotlin, convention is a technique where language features and constructs are tied to the implementation of of functions with specific names as opposed to a specific type.

Conventions is one of my favorite concepts in Kotlin, because it is a simple idea in Kotlin that allows us developers to write more readable code, while not be tied into a specific class hierarchy or interface and keeping our abstractions neater and purer.

DtsII.kt: L163

In contrast, Java has several language features that are accessible to particular classes or interfaces. So if you want to use these features, you must extend these classes or implement these interfaces.

DtsII.kt: L170

For example, if you are from a Java background you might have used the try-with-resources construct. When handling input/output in Java, you often have to ensure that you properly open and close streams. Furthermore, closing a stream often includes extra exception handling logic in case an exception is thrown during the close. The resulting code is often pretty verbose and full of boilerplate. This kind of code is necessary but also not the focus of what you developer is writing. I have often heard it referred to as "ceremony" which I feel is both poetic and appropriate.

Now try-with-resources allows you delegate the ceremony to Java. Java will do the work of properly opening and closing an input or output stream, and you can focus solely on the actual work you want to do on the stream. As you can see in this brief example, try-with-resources does the work of handling the opening and closing the BufferedReader and handling any exception that occur during opening and closing that reader. Within the try you only need to write exactly what operations you want to apply to that BufferedReader with no boilerplate.

Unfortunately, try-with-resources is only available for objects that implement the java.lang.AutoCloseable interface.

DtsII.kt: L194

Another example of this that might be familiar to you is the "for-each" loop syntax in Java. If you have a class that implements java.lang.Iterable<T> then you can unlock this looping syntax that abstracts away the ceremony of manually iterating through a collection, for example, with indices and the size of that collection. The result is less boilerplate and more readable code. But again, this nice, abbreviated syntax is only available for classes that implement java.lang.Iterable<T>.

DtsII.kt: L223

Now some of these kinds of constructs have been brought over to Kotlin. The "for-each" syntax from Java is an example. However, a huge difference is that the Kotlin "for-each" does not require java.lang.Iterable<T>.

DtsII.kt: L251

In fact "for-each" and other Kotlin features that are accessed using conventions require only a particular function name and sometimes an extra keyword modifier.

DtsII.kt: L268

To emphasize the disadvantage of language constructs requiring interfaces, I want to just take quickly go back to try-with-resources. As a Java developer, you perhaps were super happy the first time you realized you could make your input/ouput code cleaner with try-with-resources. And it seems that part of Kotlin's Java interoperability is mirroring some of the behaviors and syntax of Java, to provide familiar patterns like "for-each". Unfortunately, try-with-resources cannot be ported over to Kotlin.

The reason which is that again try-with-resources depends on the resource implementing java.lang.AutoCloseable. AutoCloseable was introduced in Java 7, but by default the Kotlin/JVM compiler produces Java 6 compatible bytecode.

If you're curious and are missing try-with-resources as you write Kotlin code, there is a stdlib extension called use which has the same effect as try-with-resources, abstracting away the ceremony required by a resource. Furthermore, because use is a higher-order extension function, when you call use it has that benefit of looking like a method of the class on which you're calling it. Also the resource handling code gets placed inside of its own block, just like try-with-resources.

Operator Overloading

DtsII.kt: L287

So let's get back to conventions and some great Kotlin features that are implemented using conventions.

Now I actually started out writing C++. One of the things that I missed most when I started writing more Java in my career was Operator Overloading. But lucky for me Kotlin has Operator Overloading, and even more lucky is that it is based on conventions.

If you look up "operator overloading" on wikipedia.org you'll see it defined as "operator ad hoc polymorphism". Operator overloading is really a type of "syntactic sugar" that allows us to take common language symbols and operators (like arithmetic operators) and assign custom behavior to them. They can be used to make code more readable and more concise.

What I realized as I played around with Kotlin's operator overloading is that conceptually they tie in nicely with extension functions and the notion of interoperability.

DtsII.kt: L321

The closest thing to operator overloading that we can see in Java is string concatenation using the "+" operator.

However, in Kotlin, we already have many overloaded operators in the Kotlin stdlib.

DtsII.kt: L339

To warn you, I really like SciFi TV shows and movies, and so many of my code examples will be trying to implement Star Trek in Kotlin.

So imagine that I am the captain of a spaceship and I'm writing a function that will allow me to organize a small crew to take on a small mission away from the ship. Well, the Kotlin stdlib provides many extension functions in a fluent API that allow me to chain together many operations on this collecton of crew members. The resulting code is readable and flows somewhat like natural language.

DtsII.kt: L366

Along with these stream/chain operations, the stdlib also provides overloaded operators for collections!

Say that I have a number of crew members. I can actually compile a list of these crew members using a "+=" (plus assignment) operator.

And if I wanted to iterate through this list of crew members and print their names, I can write a main function like this:

fun main() {
    val originalCrew = funWithTheCrew()
    for (member in originalCrew) {
        println(member.name)
    }
}

And running this main function, we can see that the "+=" worked as expected: the names printed out correspond to the two CrewMember objects that I added to me list with the "+=".

Jason Nesmith
Alexander Dane

To see how "+=" was overloaded for collections here, we can CMD + Click on the "+=" and go to its implementation.

/**
 * Adds the specified [element] to this mutable collection.
 */
@kotlin.internal.InlineOnly
public inline operator fun <T> MutableCollection<in T>.plusAssign(element: T) {
    this.add(element)
}

To make "+=" available as an operator on a MutableCollection, the stdlib has an extension function with the MutableCollection as the receiver of that extension function with the name plusAssign. plusAssign takes as a parameter a single instance of type T which is the type paramater for the collection. There is also this operator modifier keyword prefixing the function definition.

The two important things to note are:

  • The name of the extension: plusAssign
  • The operator modifier.

Operator Overloading on a Spaceship

BoldlyGoClasses.kt: L16

So what if we wanted to add operator overloading to our own classes? Say that we have a class that is an abstraction of our spaceship. It is pretty simple with just the name of my ship, the type of vessel ("vessel class"), and a list of crew members.

While the crew members is a List and List has overloaded operators in the stdlib, what if we wanted to be able to treat the Ship like a collection and use operators on the Ship itself without accessing the List of CrewMember instances directly?

Well, we can implement operator overloading on our Ship. So let's upgrade our Ship.

So again, Kotlin features that are based on conventions require the correct function name to unlock that functionality.

Say that we wanted to add the ability to use "for-each" syntax directly on our Ship rather than accessing Ship.crew and iterating over that List.

We can add a method called iterator, making sure that we prefix it with the operator keyword. We are going to simplify things by having Ship.iterator return Ship.crew.iterator and basically passing through to the underlying crew list.

DtsII.kt: L366

Now if we go back to our main function and replace the simple List of CreWMembers with an instance of our Ship class…

fun main() {
    val nseaProtector = Ship(
            name = "Protector",
            vesselClass = "Evolution Heavy Cruiser",
            crew = listOf(commanderTaggert, doctorLazerus, tawnyMadison)
    )

… and since we added that method called iterator() which returned an iterator on the underlying crew List, we can do this:

fun main() {
    val nseaProtector = Ship(
            name = "Protector",
            vesselClass = "Evolution Heavy Cruiser",
            crew = listOf(commanderTaggert, doctorLazerus, tawnyMadison)
    )

    for (member: CrewMember in nseaProtector) {
      println("${member.name} is on the ${nseaProtector.name}")
    }
}

And if we run this main, we can see that we were able to truly add a "for-each" syntax to our Ship abstraction.

Let's go back to the Ship class and add a few more things…

BoldlyGoClasses.kt: L16

Now say that we are not sure what operators we can overload and what specific function names correspond to what operators.

We can start by typing operator fun inside of our Ship class definition and if we activate Intellij's autocomplete (CTRL-SPACE), we get a useful dialog that shows us a table with function names on the left and the actual language symbol/operator on the right.

We can overload the "+=" operator by adding a plusAssign method.

operator fun plusAssign(member: CrewMember) {
    _crew.plusAssign(member)
  }

So we can add a minusAssign method to overload the "-=" operator to pair with the "+=".

operator fun minusAssign(member: CrewMember) {
    _crew.minusAssign(member)
  }

If we wanted to use the in keyword to test whether a given CrewMember is in our Ship, we can write a contains method.

operator fun contains(member: CrewMember) = _crew.contains(member)

If we wanted to provide either a way to index into the crew, similar to a regular list or array with indices, or a way to access a CrewMember by their name, similar to how we access Map values with a key, we can write two get methods.

For the first, we write a get function that takes a single Int parameter and use that to index into the CrewMember list.

operator fun get(index: Int): CrewMember = _crew[index]

For the second, we write a get function that takes a single String to accept a possible CrewMember names and then call find on the List of CrewMember instances to find one with a matching name if one exists.

operator fun get(name: String): CrewMember? = _crew.find { it.name == name }

Something important to note is that there are some restrictions on parameters and return types but these restrictions are dependendent on the method.

For example, if we tried to write contains to return an Int

operator fun contains(member: CrewMember): Int

…IntelliJ gives us an error saying that it is required to return a Boolean.

In contrast, if we wanted get to return the Int instead of a CrewMember, we can do that:

operator fun get(name : String): Int? = _crew.find { it.name == name }?.rank

But doing this also results in an error since get must have at least 1 parameter.

operator fun get(): Int? = _crew.random().rank

DtsII.kt: L366

Before we move on, let's test these new overloaded operators to see if they work! So let's use our operators in a few different ways…

  • Let's add a CrewMember before printing out the names.
  • Let's say that an unfortunate accident happened on a mission and sadly, we lost a CrewMember. (I apologize if that's a bit sad!)
  • Let's add a statement checking if a particular CrewMember is aboard the ship.
  • Let's add code to retrieve a CrewMember by name.
fun main() {
    val nseaProtector = Ship(
            name = "Protector",
            vesselClass = "Evolution Heavy Cruiser",
            crew = listOf(commanderTaggert, doctorLazerus, tawnyMadison)
    )

    nseaProtector += crewMember08

    for (member: CrewMember in nseaProtector) {
        println("${member.name} is on the ${nseaProtector.name}")
    }

    // Oops, an ill-advised away mission occurred!
    nseaProtector -= crewMember08

    for (member: CrewMember in nseaProtector) {
      println("${member.name} is STILL on the ${nseaProtector.name}")
    }

    if (techSergeantChen in nseaProtector) {
        println("That was a hell of a thing.")
    } else {
        println("Where is Tech Sergeant Chen?")
    }

    println("Second crew member: ${nseaProtector[1]}")

    val name = "Jason Nesmith"
    val crewManLarry = nseaProtector[name]
    println(if (crewManLarry != null) "Hey! $name" else "Who is $name?")
}

And if we run this main function, we can see that all the output is as we might expect:

Jason Nesmith is on the Protector
Alexander Dane is on the Protector
Gwen Demarco is on the Protector
Guy Fleegman is on the Protector
Jason Nesmith is STILL on the Protector
Alexander Dane is STILL on the Protector
Gwen Demarco is STILL on the Protector
Where is Tech Sergeant Chen?
Second crew member: EnigmaticScienceOfficer(name=Alexander Dane, rank=2, specialty=Avenging)
Hey! Jason Nesmith

I mentioned previously that this convention-based operator overloading meshes well with extension functions.

As you may know, extensions function are a Kotlin language feature that allow us to essentially extend functionality of classes without having to implement interfaces, to subclass, or to own the class that we want to extend. While extension functions are really just static utility methods in the bytecode, in Kotlin we can call them as if they were class methods. Another example of Kotlin "syntactic sugar."

Now let's look at our overloaded operators again.

Overloaded Operators via Extension Functions

BoldlyGoClasses.kt: L16

All of the operator overloading we just did was through class methods. But what if we did not own this class?

No problem! In the same way that we can use extension functions to extend the functionality of our APIs that we do not own.

To demonstrate this I'm going to use some interesting context actions available in IntelliJ. If I put the cursor on our contains method and hit OPTION-ENTER, I see an option to "Convert member to extension". I'm going to select that, and IntelliJ has now converted contains from a member method to an extension function.

operator fun Ship.contains(member: CrewMember) = _crew.contains(member)

There is a compiler error, because now that contains is an external extension function, it no longer has access to the private _crew property. But we can fix that.

operator fun Ship.contains(member: CrewMember) = crew.contains(member)

You can see this looks more like the way that the operator overloading is done for collections in the Kotlin stdlib.

This means that we can leverage operator overloading for library classes and other classes that we do not own.

Now there is another reason to consider using extension functions for operator overloading even if we do own the class to which we want to add operator overloading.

Whenever I have spoken to other developers about extensions functions, I always used to talk about the idea of extending functionality of existing classes without subclassing. However, I learned an additional reason for extension functions.

There is a great Coursera course by Andrey Breslav and Svetlana Isakova called "Kotlin for Java Developers". Even though it's probably meant for Kotlin beginers, there is lots to learn as an intermediate to advanced Kotlin developer. There are several sections in the course where Andrey answers questions about the purpose of Kotlin features and how they should be ideally used.

It was from one of these sections that I learned that part of the motivation behind extension functions is to allow developers to write cleaner abstractions. You can keep only the most intrinsic and vital characteristics of an abstraction inside of a class, and then use extension functions to keep things like utilities and helpers outside of the class definition while still being able to use member method calling syntax.

I think this is a very compelling reason to use extension functions for operator overloading, because you can see how many of the operators that we just added are really just utilities: they don't impact the meaning and intrinsic properties of my Ship class. They're just useful.

Thinking about extension functions this way, reminds me again of those pragmatic principles of readability and reuse that we spoke of earlier.

DtsII.kt: L392

How Does Operator Overloading Work in the Bytecode?

So you might be curious whether Kotlin is doing anything special to make conventions and operator overloading work. To find out what is actually happening when we use conventions and operator overloading, let's take what I like to call a "Bytecode Break!" and look at what the Kotlin compiler generates from this code.

BytecodeOperatorOverloadng.kt

In this file is a subset of the code that we just ran in our main function with our new overloaded oeprators. Now if I initiate the "Show Kotlin Bytecode" action and then "Decompile" back to Java (since I personally find it easier to read than assembly).

And in the bytecode we can see that there is no magic here. Operator overloading implemented as member methods are called as member functions. Operator overloading implemented as extension functions are called as static Java functions,

Now this actually provides some benefits that play into the "interoperability" aspect of our pragmatic principles.

Operator Overloading Extension Functions in Java

DtsII.kt: L413

If we choose to implement overloaded operators as extension functions, we can call them from Java as static functions, since that is what they are underneath.

BoldlyGoKotlnFromJava.java.kt

This is a Java class that is using my Kotlin-based Ship class. While Java can't give me the convenient operator syntax from Kotlin, I'm still able to leverage the utility of the function used to implement an operator.

So I can add the following call to call on the contains method that I wrote to implement he in operator.

if (BoldlyGoClassesKt.contains(andromeda, captain)) {
  System.out.println("Hey, " + captain.getName() + "!");
}

This can be very useful if you are still in the process of migrating a Java codebase to Kotlin. This helps you migrate pieces of business logic to Kotlin even if some of the users of that logic are in Java still. At the same time you can utilize the "syntactic sugar" in Kotlin. Similarly, you can feel free to write new code with syntactic sugar and not worry that using something that's a Kotlin-only feature prevents inteoperability. It does not.

BoldlyGoJavaOperatorOverloading.kt

Overloading Operators on Java Classes

In fact, there's an opposite but helpful side to the interoperability here, because if you need to use an existing Java class in Kotlin, you can add extension functions for that Java class that implement overloading. Again this is nothing different than we spoke of before about adding functionality to a class that we don't own and don't want to subclass except that we can apply this across the Kotlin-Java border and then also add in operator overloading on the Kotlin side.

In this file, we can see that there is this JavaShipclass. And if we click through…

JavaShip.java

… we see that it is simply a Java version of our Ship class.

Now as we mentioned, Java does not have operator overloading. But if we're using a Java class in Kotlin, we can give that Java class some operator overloading…

BoldlyGoJavaOperatorOverloading.kt

…through extension functions!

operator fun JavaShip.compareTo(another: JavaShip): Int {
  return name.compareTo(another.name)
}

operator fun JavaShip.plusAssign(member: JavaCrewMember) {
  crew = crew.plus(member)
}

To think again on our pragmatic principles, we can obviously see the interoperability here, leveraging a Java class in our Kotlin code but with the convenience of Kotlin syntax which can improve readability.

In this example, we get to leverage understood language symbols for comparison and for assignment:

val bestJavaShip = if (essCappuccino > nccCaPheSuaDa) essCappuccino else nccCaPheSuaDa
println("The Best Java Ship™ is… $bestJavaShip")
essCappuccino += joe
println("${essCappuccino.crew.size} crew members!")

Pragmatic Operator Overloading

On that note, I think it's worth thinking carefully about why and how we are using operator overloading. The syntax is definitely fun and more concise, but it is possible to abuse operator overloading like any other language feature to the point where we start violating our pragmatic principles where perhaps code becomes less readable, less reusable, etc.

In the Coursera course that I mentioned previously, one of the question that Andrey Breslav's answers is when to use and when not use operator overloading.

https://www.coursera.org/learn/kotlin-for-java-developers/lecture/a1bX5/not-using-operator-overloading

To paraphrase what he said, having access to custom behavior through features like operator overloading is very powerful, but there is a reason why the set of operators that can be overloaded has been limited: to prevent rampant abuse of this feature. Andrey encourages us to use operator overloading in a natural way, following common patterns, and to not go against the existing meaning of these operators. The example that he gives is overloading / to be a slash operator for a path class which is essentially creating a new syntax on top of an established use of that symbol. Stay away from these kind of abuses.

Referring again to those pragmatic principles, we could argue that doing something like this violates reusability because of the fact that it's changing commonly understood semantics.

But with the proper care and consideration, of course, operator overloading can be extremely useful and extremely fun!

Next let's talk about some of the other convention-based features that you can leverage in Kotlin.

Destructuring Declarations

DtsII.kt: L466

Destructuring declarations is a way to declare and initialize multiple variables from multiple component values of a single object. You probably have already used them or at least have seen this syntax:

val (first, second) = "One" to "Two"

In this example the right-hand side is in fact a Pair with two properties: first and second. Using a destructuring declaration, in one line we can declare two values, also called first and second on the left-hand side and initialize them to the corresponding values of the two Pair properties.

The declarations do not have to match the names of components on the right-hand side. We can easily do something like this:

val (a, b) = "One" to "Two"

That being said if the compiler thinks that we might have misordered the declarations, it will tell us:

val (second, first) = "One" to "Two"

Here, Pair.first would be assigned to a val second, and Pair.second would be assigned to val first. The compiler detects the match of the names and also the odd ordering and warns us.

Destructuring declarations can be used in many places, including key-value access in a map. We could iterate through the key-value pairs in a Map with the following:

for (entry in fleetMap) {
  println("The ${entry.key} is a ${entry.value.vesselClass}")
}

However, we can make this more readable by using a destructuring declaration:

for ((name, ship) in fleetMap) {
  println("The $name is a ${ship.vesselClass}")
}

Can we add the capability for destructuring declarations to our own classes? Yes!

BoldlyGoClasses.kt: L16

The particular values that are pulled out of an object and the order in which they are assigned are fixed when you add destructuring declarations. You can see why in how we add them.

For each component that may be extracted from you class, you need write a function with the following signature:

operator fun componentX(): T

  • where X is the order in which destructured values need to appear on the left-hand siz
  • where T is the return type for the destructured value

Here, we're doing something very basic and just listing out all the Ship properties.

operator fun component1(): String = name
operator fun component2(): String = vesselClass
operator fun component3(): List<CrewMember> = crew

Using these destructuring declarations would look something like this:

DtsII.kt: L466

val (name, vesselClass, crew) = enterprise
println("The $name is a $vesselClass with ${crew.size} members.")

Destructuring Declarations in Data Classes

If you are using a data class, you get destructure for free for each property in the data class.

https://github.com/queencodemonkey/dissecting-kotlin- considering-conventions/blob/master/src/BytecodeDataClassComponentN.kt

Here we have a data class with many different properties. To show you that among the many things that the data class generates for you are also component functions, we're going to take another "Bytecode Break!"

So we shall go to the "Show Kotlin Bytecode" action again and decompile.

If we scroll down a bit we can see 11 component functions that the data class provided to us for free.

And just to reiterate that these are done by conventions: we just need to implement the N functions with the right name and the operator keyword.

Infix

DtsII.kt: L548

The first time I became aware of Kotlin conventions was through infix notation.

Infix is the ability to make a function a call while omitting both the dot and the parentheses if the function meets certain criteria:

  • The function has to be a member function or an extension function.
  • The function may only have one parameter; default values and vararg are not allowed
  • The function must have the infix notation keyword in its declaration.

DtsII.kt: L575

If you ever used downTo or the "shift-right" operator, those are not operators. They are in fact infix functions, which we can see by just hovering over these operators in IntelliJ.

DtsII.kt: 606

We can write our own "binary operators" using infix.

Let's go to the class that represents a crew memer of our spaceship…

BoldlyGoClasses.kt: L45

What if we wanted to write a helper method that made easy to transfer a crew member to different ships. We might write two methods like this:

sealed class CrewMember {
  abstract val name: String
  abstract val rank: Int

  infix fun assignedTo(ship: Ship) {
    ship += this
  }

  infix fun transferredFrom(ship: Ship) {
    ship -= this
  }

…where one method adds the CrewMember to a ship and one method removes the CrewMember from a ship.

Both of these are class methods, with a single parameter, and since we have prefixed the declarations with infix, we can call these methods on a CrewMember and Ship like so…

fun main() {
  jeanLucPicard assignedTo ncc1701d
  worf transferredFrom ncc1701d

  var isHere = jeanLucPicard in ncc1701d
  println("Is there a John Luck Pickerd here? ${if (isHere) "YES!" else "NO!"}")

  isHere = worf in ncc1701d
  println("Is Worf here? ${if (isHere) "YES!" else "NO!"}")
}

We can see that using infix makes the method look more like a binary operator on the receiver object. Depending on we named that method, we can see that it might makes the call similar to a natural language way of speaking, potentially more fluent.

To be honest, I have rarely used infix in production. I think ti's a great feature, but let's look at it compared to other ways of writing the same effect in our code.

I could achieve the same thing but with the "+=" operators that we already have implemented.

The question to ask here if we want to follow our pragmatic principles is which is more readable and reusable? The answer may not always be straightforward and may be subjective.

I would just encourage you to think seriously on the intent of what you're doing and try to think seriously on whether these decisions do make your code more readable, more reusable, or help with interoperability.

Delegated Properties

Before I wrap up, I'd like to talk about one convention-based feature that I have used in production code and those are delegated properties.

DtsII.kt: L618

The Delegation Pattern is a common design patterns where you basically let someone else (a delegate or a helper) do some piece of work.

Delegated Properties in Kotlin allow us to use the delegation pattern when it comes to the logic inside of property getters and setters. They allow us to abstract custom accessor logic into a separate class and for that custom logic to replace the default getter and setter generated by Kotlin. Delegated Properties are great for repeated logic in accessors and in fact, the stdlib has some delegated properties for common patterns like lazy initialization, observable values, etc.

There are two ways to implement Delegated Properties: through interfaces or by convention.

DtsII.kt: L647

In this example, SillyNameDelegate is a delegate for the getter and a setter of a nullable String property. By simply having this delegate class implement getValue and setValue and ensuring that the apropriate types in each match up, we can add a property to our class SillyUser which delegates implementation of a name property to the SillyNameDelegate by simply following the property declaration with a by keyword and an instance of the delegate.

Going back to our pragmatic principles here we can see that there is a sense of reusability by the ability of a delegate to encapsulate reusable accessor logic. And also leveraging delegates is done in a fairly readable way using the by syntax.

Wrap Up

Now all of the features that we discussed today are interesting, fun, and have potential to be powerful. Does using these features instantly make your code more idiomatic? Not necessarily. And using them thoughtlessly I think would have the opposite effect. I would say that the more I use Kotlin and gain experience, while I still enjoy learning about new and interesting language features, I try to temper the enthusiasm for fun syntax with very careful consideration on what I am trying to accomplish by using these features, and thinking on the pragmatic principles of the Kotlin team and ask if I'm thinking about these features pragmatically.

And while it might be hard to know always if what we're doing is idiomatic for Kotlin, by thinking careful and being pragmatic we hopefully will get to better Kotln.

DtsII.kt: L730

Thank you so much!