Skip to Content

What's New In Kotlin 1.3?

A comprehensive-ish overview of new language features in Kotlin 1.3

Posted on

Kotlin 1.3 is finally here with some great new features. In this post I will go over most of the new features and how to use them. I’ve tried to make this as comprehensive as possible without getting too detailed, and provide examples to make things clearer. I intentionally left out covering every single thing as I wanted to cover only the new language features. I’ve also tried to cover some of the new experimental features as best as I can but due to its nature, things might change over time and obviate some of this information. Stable features are covered towards the beginning, and experimental features are down at the end.

Let’s start at the beginning, shall we?

New main() Signatures

Kotlin 1.3 adds two new ways to define the main method. Now we can define it without parameters, and this is nice because a lot of the time I don’t read them anyway:

fun main() {
    // Valid now!
}

If we are using coroutines, we can define a suspending main function:

suspend fun main() = coroutineScope {
    // Some kind of suspenseful fun stuff in here!
}

Coroutines Have Graduated to Stable

Coroutines have been experimental since 1.1, and they’ve finally graduated to stable! This means we can use them without fear that the API will have large changes from release to release. For the record: “experimental” never meant unstable, it has always meant that the API could change, and it did for 1.3. I feel like we should buy JetBrains and specifically Roman Elizarov (the project lead) a graduation present of some kind because this is a fantastic achievement.

Let me mention here that I am only going to cover the most recent changes to coroutines, and even then, only at a very high level. For an introduction to what coroutines are and how to effectively use them, I encourage you to read the comprehensive and well written documentation. Covering all of coroutines could probably take an entire book, let alone one blog post!

Structured Concurrency

The major change to coroutines is a move to what’s called “structured concurrency”. Structured concurrency recognizes the fact that applications don’t generally launch coroutines that last the entire lifetime of the application. They are usually bound to the lifecycle of something else like a UI element, request, or another coroutine. What happens if a user gives up on a request and tries again? Or closes and reopens a window that had kicked off a bunch of coroutines? Now we have coroutines off doing work we don’t care about, and taking up system resources. It can be difficult to write correct code when dealing with this situation.

With structured concurrency, all launched coroutines must take place within a CoroutineScope, making it much easier to handle failure cases. This interface is implemented by the classes the coroutines lifetimes are bound to (e.g. a UI element or a request), and provide a context for them to run in. Now, it’s a simple matter of launching our coroutine within a CoroutineScope and it does the heavy lifting of canceling coroutines when the scope ends.

An often cited example of coroutines is something like this:

suspend fun doSomeWork(): Int {
    val answer1 = async { doSomeLongRunningWork(42) }
    val answer2 = async { doSomeLongRunningWork(806) }
    return answer1.await() + answer2.await()
}

In this example, we have two coroutines doing some work for us asynchronously. Then we wait for them to return some data, and presumably do something useful with it. Now this code looks pretty simple, but there are concerns. What happens if one of our asynchronous calls fails? The other one is still going, doing useless work. It’s not straight forward to clean up from a case like this - we would have to write a lot more code!

Structured Concurrency to the rescue! By wrapping our code in a coroutineScope, our asynchronous function calls both become children of the scope. So if one fails now, the other one is canceled. The best part is this code is really simple, and does so much of the heavy lifting for us!

suspend fun doSomeWork(): Int =
    coroutineScope {
        val answer1 = async { doSomeLongRunningWork(42) }
        val answer2 = async { doSomeLongRunningWork(806) }
        answer1.await() + answer2.await()
    }

In short, Structured Concurrency makes it easier to do the right thing, and reason about how failure will be accounted for. Roman Elizaov wrote a good blog post that goes into more of the reasoning behind structured concurrency, I urge you to check that out.

Sequences

There are also changes in how sequences are built. If you were using buildSequence before, it has been renamed to sequence. This feature allows us to generate a lazy sequence, using coroutines. This will suspend when values are not needed, and end appropriately when the sequence is no longer being used or is exhausted.

For example, to create a lazy sequence of Fibonacci numbers, we could write it like this. It will only generate numbers as needed, and stop when consumption has finished, thanks to the fact that yield is a suspending function, and sequence, which does all of the coordination:

fun lazyFib(): Sequence<Int> = sequence {
    var state = Pair(0, 1)
    while(true) {
        yield(state.second) 
        state = Pair(state.second, state.first + state.second)
    }
}

Now that coroutines are stable, I suspect we will see more people taking advantage of them. I personally would like to see some work being done on actors using coroutines/channels. I’m excited to see what interesting uses the community comes up with!

Capture when’s Subject in a Variable

One of the limitations of using when is the inability to use the value being considered by when, and only within that block. Meaning, we’ve had to write code like this:

val answer = theAnswer()
return when(answer) {
    42 -> "You got the right answer!"
    else -> "Sorry, $answer is not correct"
}

And thats fine, but answer is in scope for more than just the when block, and it is not a single expression (which is always nice).

Starting in Kotlin 1.3, we can capture the value being considered in when into a variable that is only in scope for the when block! Now we can rewrite our example above into a single expression!

when(val answer = theAnswer()) {
    42 -> "You got the right answer!"
    else -> "Sorry, $answer is not correct"
}

I can see myself using this immediately.

Multiplatform Random

Generating random numbers on the JVM is not an issue, but this has been lacking on the other platforms that Kotlin supports. 1.3 brings a new random implementation that works cross platform. To do this, the default implementation uses the XORWOW algorithm, but since Random is an interface, we can implement it differently if the need arises.

import kotlin.random.*

val random = Random.nextInt(10)    // yields 0-9

Another nice feature is the random() extension functions for collections, ranges, and arrays:

// Pick a random element from a List
val contestants = listOf("Todd", "Anna", "Emma")
println("Winner: ${contestants.random()}")

// Generate a String or random letters
val randomWordLetters = 'A' .. 'Z'
val randomWord = (0..10).map { randomWordLetters.random() }.joinToString(separator = "")
println(randomWord)

More detail on how this was developed and what ideas were considered can be found in this KEEP.

Functional Type Arity Limit is Now 255

For years, we’ve suffered through with only 22 parameters for functional types, even thought the JVM technically supports 255. Well finally our code reviews will go smoothly - go ahead and check in that 255 argument lambda and issue a PR! (I’m totally kidding, don’t do that, or don’t blame me if you do).

The interesting part about this change is how they did it. In Kotlin, functional types are represented by generic classes indicating their artity, parameter types, and return type. Internally, Kotlin turns our lambda into an instance of Function0<R>, Function1<P0, R>, Function2<P0, P1, R>, all the way up to 22. So why not just do that for arities 23 through 255? Well, I suppose the authors of Kotlin could have done that, but it seems like a lot of duplicate code.

For arities 23 through 255, Kotlin will wrap our parameters into an instance of FunctionN:

@SinceKotlin("1.3")
interface FunctionN<out R> : Function<R> {
    operator fun invoke(vararg args: Any?): R
    
    override val arity: Int
}

Under the covers, Kotlin’s compiler will do the right thing with turning the array of Any into individual arguments, and ensuring that the arity of the array and the function length still match. There is a very good KEEP Discussion on this that goes into greater detail, if you are interested.

More Consistently Available isNullOrEmpty and orEmpty Functions

Kotlin has had support for isNullOrEmpty() on CharSequence (Strings) since early days, and now adds it to nullable Collections, Maps, and Arrays. As its name suggests, true is returned if the receiver is null or empty.

val maybeArray: Array<String>? = calculateArray()
maybeArray.isNullOrEmpty()

val maybeCollection: Collection<String>? = calculateCollection() 
maybeCollection.isNullOrEmpty()

val maybeMap: Map<String, String>? = calculateMap() 
maybeMap.isNullOrEmpty()

And similarly, Kotlin has had support for orEmpty() on Strings, Collections, Maps, and Arrays and adds it to nullable Sequence. If the receiver is null, an empty version of the receiver is returned instead. This helps us get rid of nullable collections or sequences with a sensible default.

fun makeNullableSequence(): Sequence<String>? = ...

val maybeSequence: Sequence<String> = makeNullableSequence().orEmpty()

ifEmpty and ifBlank Functions

If you are a fan of .takeIf(), you’ll enjoy this. CharSequence, Collections, Map, and Array (just the object arrays, not type-specific) have a new function called .ifEmpty(), which executes a lambda if the receiver is empty.

Example:

// Prints the list of features, or a default list if none are present.
val listOfFeatures: List<String> = ...
println(listOfFeatures.ifEmpty { listOf("DefaultFeature1", "DefaultFeature2") })

CharSequence (which String implements) also gets an .isBlank() function, which does the same as .isEmpty(), but only if the sequence contains all whitespace.

// Prints the userProvidedValue or "<Blank!>"
val userProvidedValue: String = ...
println(userProvidedValue.ifBlank { "<Blank!>" })

These seem like nice additions, like how I would want the ?: operator work in these cases. I wonder if there’s a need for .isBlankOrEmpty() on CharSequence as well?

hashCode() for Nullable Types

A common practice in Kotlin is to write code like this, to generate a default hash code for a nullable type:

// Set myHashCode to 0 if myObject is null, otherwise get myObject's hash code.
val myHashCode: Int = myObject?.hashCode() ?: 0

Kotlin 1.3 adds this to the standard library:

public inline fun Any?.hashCode(): Int = this?.hashCode() ?: 0

And with that, we can turn the example we had above into this:

val myHashCode: Int = myObject.hashCode()

I suspect a lot of us have defined our own extension for this, so be sure to drop that code once you move to 1.3.

Boolean Companion

Have you ever wanted to add an extension function to Boolean, only to find out you can’t because it doesn’t have a companion object? For example, I wanted to add a random() function on Boolean once and couldn’t because there was no companion. Now we can write this pretty easily:

fun Boolean.Companion.`¯\_()_/¯`(): Boolean = Random.nextBoolean()

val majorLifeDecision = Boolean.`¯\_()_/¯`()  

I should note that code-wise, this is probably the smallest of thew features in Kotlin 1.3:

// In kotlin.Boolean: 

@SinceKotlin("1.3")
companion object {}

New Constants in Basic Types

We can now get the SIZE_BITS and SIZE_BYTES of Char, Byte, Short, Int, and Long (as well as the unsigned types). This doesn’t strike me as overly handy, but I haven’t done a lot of multiplatform work so possibly this is of use there. Char also has MIN_VALUE and MAX_VALUE defined as well.

println("Char:  ${Char.SIZE_BITS} bits, ${Char.SIZE_BYTES} bytes")
println("Byte:  ${Byte.SIZE_BITS} bits, ${Byte.SIZE_BYTES} bytes")
println("Short: ${Short.SIZE_BITS} bits, ${Short.SIZE_BYTES} bytes")
println("Int:   ${Int.SIZE_BITS} bits, ${Int.SIZE_BYTES} bytes")
println("Long:  ${Long.SIZE_BITS} bits, ${Long.SIZE_BYTES} bytes")
println("Char:  ${Char.SIZE_BITS} bits, ${Char.SIZE_BYTES} bytes")

Char:  16 bits, 2 bytes
Byte:  8 bits, 1 bytes
Short: 16 bits, 2 bytes
Int:   32 bits, 4 bytes
Long:  64 bits, 8 bytes
Char:  16 bits, 2 bytes

These are also available on Unsigned Types (see below).

Nested Declarations in Annotations

When declaring annotations in Kotlin, you can now nest other annotations, enumerations, and a companion. This might make it easier to encapsulate some of your logic.

annotation class Transaction(val isolation: Isolation) {
    enum class Isolation { CREATE_NEW, REUSE_EXISTING, NONE }

    companion object {
        val maxThreads: Int = 8
    }
}

@Transaction(Transaction.Isolation.CREATE_NEW)
class Example() {
    init {
        println("System Max Threads: ${Transaction.maxThreads}")
    }
}

Or maybe you could provide a few annotations and use the outer as a namespace:

annotation class Settings {
    annotation class Name(val name: String)
    annotation class PasswordStrategy(val minChars: Int, val minComplexity: Double = 1.0)
}

@Settings.Name("Some kind of provider")
@Settings.PasswordStrategy(minChars = 10)
class Provider {

}

I honestly don’t know where the sweet spot is with this feature, so I’m looking forward to how the community uses this and what eventually becomes idiomatic usage.

Sealed Class Reflection

A nice addition to the reflection library is the ability enumerate all of the subclasses of a sealed class.

// Given a sealed class structure (borrowed from the Kotlin documentation):
sealed class Expr
data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()
object NotANumber : Expr()


// Print the subclasses, given a KClass reference:
val exp: KClass<Expr> = Expr::class
println(exp.sealedSubclasses.map { it.simpleName })

// Prints: [Const, Sum, NotANumber]

I should mention that I’m just printing the names here, but sealedSubclasses gives us KClass instances, so we can do whatever reflection we want on those as well.

New Compiler Arguments

Argument File

In Kotlin 1.2.50, the -Xargfile=filename.txt argument was added to kotlinc. This lets us define our compiler arguments in a file (called filename.txt in this case). Put each argument on its own line and the compiler will use those. This is really handy if there are a lot of arguments.

Kotlin 1.3 makes this more useful by allowing @filename.txt as a shortcut, so we don’t have to remember the -Xargsfile part. That’s something I like because even after 23 years of Java, I have to look up command line switches all the time.

Progressive Mode

This was also introduced in Kotlin 1.2.50 as experimental, and has graduated to stable in 1.3. By adding -progressive or -Xprogressive as an argument to the compiler, we can take advantage of certain fixes before waiting for a major release. Kotlin tries very hard not to break things when fixes come out, so sometimes they must wait for a major release. If you work on an active project that keeps up with compiler upgrades, you can enable progressive mode to take in some of those fixes early. In exchange for early fixes, you might have code that suddenly has deprecated features - so be careful. This is a nice thing to have, and I’m glad its an option.

Experimental Annotations

Irony Alert! This feature is Experimental and must be enabled explicitly.

If we want to use any of the experimental features in Kotlin 1.3 (see below!) we’ll need to explicitly enable them, probably in our build file. It’s a simple procedure but doesn’t exactly make a strong connection between code and configuration. Also shipping in Kotlin 1.3 are two annotations (@Experimental and @UseExperimental) that we can use to build other annotations which turn on experimental features in code, rather than configuration. We can use this to define and enable our own experimental features!

Let’s say we have a new API, and we might want to change it in the future. We have a couple of options in order to inform our users that this is experimental and must be opted in to. The first example is called a “propagating opt-in”, meaning if we opt-in to consuming an experimental API, our consumers need to opt in as well. This is to prevent experimental features from bleeding out of our class inadvertently.

// Define this to represent our new experimental feature
@Experimental  // <----- NEW!
annotation class OurNewApi 

// Tag our experimental code with the annotation
@OurNewApi
class NewApi {
	...
}

// And when somebody wants to use it, they have to opt in.
// Because we have our consumer pass a NewApi, they'll need to opt in to @OurNewApi as well!
@OurNewApi
fun doSomethingWithNewApi(api: NewApi) {
	api.doSometihng()
}

On the other hand, if the experimental feature doesn’t bleed out of our code and into the callers, we can use what’s called a “non-propagating opt-in”. This lets us use the experimental feature without having to have our clients opt in, just our code.

// We define @OurNewApi,  and tag our code as above.

// When somebody wants to use it, they have to opt in.
// Use of NewApi is encapsulated, we don't need our consumers to opt in.
@UseExperimental(OurNewApi::class)   // <----- NEW!
fun doSomethingEncapsulatedWithNewApi() {
	NewApi().doSomething()
}

These annotations can be applied to more or less anything, including extension functions.

There is a very good overview of this in the KEEP.

Inline Clasess

This feature is Experimental and must be enabled explicitly.

Sometimes when I write code I want a type for something simple, to signal to my users that they need to consider more than just the underlying type. For example, if we had a function that takes a person’s age, we might want a very lightweight wrapper class:

class Age(val age: Int)

fun canVote(age: Age) = age.age >= 18

And that works, it makes our uses think of Age instead of say, Height, for example. The issue is that it creates GC pressure - we create and throw away trivial Age objects, just to make types easier to express. Inline classes are the solution to this problem (and others!). They are, as their name suggests, inlined. They are never instantiated. This allows Kotlin to enforce their type at compile time but depend on the underlying type (Int in this case) at runtime. The best of both worlds. Let’s rewrite our example:

inline class Age(val age: Int)

fun canVote(age: Age) = 
    age.age >= 18

That looks almost identical except for the soft keyword inline. We gain compile time type safety, and give up having to instantiate Age at all. That’s a great trade!

We can also add functions and operators to our inline classes, and call them from other classes as if they were typed:

inline class Age(val age: Int): Comparable<Age> {
    fun canVote() = 
        age >= 18
}

class Person(val age: Age) {
    fun vote(candidate: Candidate) {
        if(!age.canVote()) {
            throw IllegalStateException()
        }
        recordVote(candidate)
    }
}

And we can call this from Java or Kotlin:

// Java
final Person person = new Person(21);
person.vote(new Candidate("Todd"));
// Kotlin
val person = Person(Age(21))
person.vote(Candidate("Todd"))

As I’ve shown, we’re still using the underlying types from Java, but from Kotlin we have a nice type-safe wrapper that never gets instantiated. There are quite a few limitations to Inline Classes:

  • Must have a public constructor with a single val (the underlying value we really want to use)
  • No init block
  • Must be final
  • Cannot extend anything - can only implement interfaces
  • Cannot have any additional properties - we only get the underlying specified in the constructor
  • Must be a top level class
  • Cannot have inner classes
  • Cannot be defined with recursively defined generics

Despite these limitations, I feel that once this feature graduates to stable it will be one of the things that many great Kotlin libraries will depend on. It has a lot of potential to enhance type safety in Kotlin code without causing GC pressure. As with most new features, there is a very good KEEP describing the thought process behind inline classes, and providing examples of why certain things aren’t allowed.

And because we now have inline classes, we can also have…

Unsigned Types

This feature is Experimental and must be enabled explicitly.

We are no longer exclusively shackled to the sign bit in Kotlin! All of the integer number types have corresponding unsigned types now: UByte, UShort, UInt, and ULong. What’s interesting is this feature is implemented entirely with inline classes! That means is the unsigned types are represented internally by their signed counterparts, but the inline classes hide that detail from us. Adding in a new set of types like this required a lot of additional support classes (ranges, types arrays, etc) and extensions.

I’ll admit, when I heard that Kotlin 1.3 would have unsigned types, I thought “why bother”. But now that I see how they were implemented so simply with inline classes, I find it inspiring and a great example for how to use that feature. Having an unsigned type is going to be nice for networking code or domains where having only positive numbers is a feature.

Let’s go through some usage examples.

// Literal shortcode for UInt
val aUInt: UInt = 3u
val aULong: ULong = 3uL

// And from Hex...
val hexUByte: UByte = 0xFFu  // 255
val hexUShort: UShort = 0xFFFFu  // 65535
val hexUInt: UInt = 0xFFFF_FFFFu  // 4294967295
val hexULong: ULong = 0xFFFF_FFFF_FFFF_FFFFu  // 18446744073709551615

// Conversion from signed to unsigned
val myUByte: UByte = someByte.toUByte()
val myUShort: UShort = someShort.toUShort()
val myUInt: UInt = someInt.toUInt()
val myULong: ULong = someLong.toULong()

// Conversion from unsigned to signed may result in negative numbers!
val someOtherByte: Byte = myUByte.toByte()
val someOtherShort: Short = myUByte.toShort()
val someOtherInt: Int = myUByte.toInt()
val someOtherLong: Long = myUByte.toLong()

// String to unsigned
val uByte: UByte = "255".toUByte()
val uShort: UShort = "65535".toUShort()
val uInt: UInt = "4294967295".toUInt()
val uLong: ULong = "18446744073709551615".toULong()

As I mentioned, there are also typed arrays to match their signed counterparts, and handy functions to create them:

val uByteArray: UByteArray = ubyteArrayOf(1.toUByte(), 2.toUByte(), 3.toUByte())
val uShortArray: UShortArray = ushortArrayOf(1.toUShort(), 2.toUShort(), 3.toUShort())
val uIntArray: UIntArray = uintArrayOf(1u, 2u, 3u)
val uLongArray: ULongArray = ulongArrayOf(1uL, 2uL, 3uL)

And ranges for UInt and ULong:

// Range of unsigned Int
(0u .. UInt.MAX_VALUE).forEach {
    println("Unsigned Integer: $it!")
}

// Range of unsigned Long
(0uL .. ULong.MAX_VALUE).forEach {
    println("Unsigned Long: $it!")
}

Thanks to Kotlin’s support for operators, all of the mathematical operators we have on the existing signed number types can be found and used on the new unsigned types, and between signed and unsigned types.

There is a great design discussion in this KEEP document, further illustrating all of the features. There are also several unanswered questions, which are detailed there as well.

Contracts

This feature is Experimental and must be enabled explicitly.

Return Value Contracts

The Kotlin compiler is pretty good at figuring out when things can and can’t be null but there are still limitations. Let’s look at an example from the Kotlin standard library, CharSequence?.isNullOrBlank(). A straight forward implementation might look like this:

fun CharSequence?.isNullOrBlank() : Boolean = 
   this == null || this.isBlank()

And we can make a call to it like this (keeping in mind that String implements CharSequence):

val s: String? = calculateString()
if(!s.isNullOrBlank()) {
    s?.reversed()
//   ^---------------- Why?!    
}

Despite the fact that a String? can’t actually be null if isNullOrBlank() returns false, we still have to use the safe traversal operator to call reversed() on our String?. If we could somehow express this to the compiler, it would be able to smart cast that for us. This is what Contracts allow us to do.

Let’s see how isNullOrBlank() in the standard library is rewritten using a contract:

inline fun CharSequence?.isNullOrBlank(): Boolean {
    contract {
        returns(false) implies (this@isNullOrBlank != null)
    }

    return this == null || this.isBlank()
}

In this case, we’re entering into a contract with the compiler. Namely, if we return false, the compiler is safe to assume that the CharSequence? we are looking at is not null, and it can be smart-casted to a CharSequence. We can put our smart cast to use, without that ?.:

val s: String? = calculateString()
if(!s.isNullOrBlank()) {
    s.reversed()   // Look! Smart cast at work!
}

Isn’t that handy?

Calls In Place

Here’s another example concerning lambdas:

// < v1.3

fun main(args: Array<String>) {
    val myValue: String
    run {
        myValue = "Hello, World!"   // Compile Failure!
    }
}

If we try this in Kotlin 1.2 or lower, we get a compiler error: Captured values initialization is forbidden due to possible reassignment. This means we can’t assign the val myValue because the compiler can’t guarantee that run will only execute the lambda we give it one time and one time only. We could possibly not assign it at all, or assign it twice. Contracts to the rescue! In 1.3, run has been reimplemented like this:

public inline fun <R> run(block: () -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

This callsInPlace contract makes that guarantee we needed above. Namely, that the lambda provided to run will be called once and only once. For the curious, we could also specify AT_MOST_ONCE or AT_LEAST_ONCE, should we need them.

There are plenty of other things that contracts can do, and JetBrains have started using them through the standard library and in kotlin-test. To use them in our code, we have to enable the feature via the @ExperimentalContracts annotation, or a compiler switch until this feature graduates to stable. This is just the tip of the iceberg with contracts, there is a good design overview in this KEEP.

In Closing

Wow, a lot of work has gone into Kotlin 1.3 in the past year. While some of the more interesting things are still experimental, I’m encouraged by the graduation of coroutines to stable. I have no doubt we will see more innovation and improvements from JetBrains and the community for 1.4.

Congratulations to everyone who worked on 1.3, it really is a fantastic release.