inline language

Inline classes, the next level of type safety

Posted on by Adrian Bukros

The aim behind the design of a modern programming language (amongst many things) is to steer developers away from making mistakes so they can deliver robust applications. Many features of the Kotlin language are driven by this philosophy. Elevating null safety to the language level is a great example of this intention. In this article, I'm going to show you an upcoming feature that can also help us to avoid mistakes.

The problem

Let's start with an example. You have to implement a login() function that allows users to access the application by providing their user name and password.
How would you approach the task? I guess you would declare a function like this at some point of your coding process:

fun login(username: String, password: String): Boolean {}

Easy, right?

A simple Kotlin function that accepts a username and password and returns a Boolean type that represents the succession of the login attempt.

val username = "name"
val password = "asdasd"

login(username, password)

For a first version, it's not bad actually. However, there is a big chance that in the future it can blow up. How? I will show you!

What prevents the programmer from calling the login() function like this?

login(password, username)

or like this?

login("asdasd", "username")

Nothing, right? Small details like this can be easily overlooked and our function doesn't raise its voice against misuse. It accepts two String as arguments and it got two String.

Of course, you can prepare your login() function to try to filter out wrong input parameters but there is still a possibility that the user will only see an Authentication error in the end. Also, you can only protect during runtime which should be always the last resort.

If we could prevent the programmer from even writing such a thing and catch the error in compilation time that would be the real thing.

Sort of a solution

We are now aware of the possible error. Let's try to modify our implementation so that error can never happen.
In a statically typed language, our best friend is the compiler that can make sure we can only get the type we expect as input for our functions.
We should use this to our advantage. How about if we make a wrapper around the username and password instead of just using the String type?

class UserName(val value: String)
class UserPassword(val value: String)

Hmm. That could work, but wrapping every data like this into a class could be bad for performance. Assuming that we are running on the JVM, to have data as an instance of a class has serious overhead compared to the use of primitive types.

I have good news. With Kotlin you can solve this issue.

Inline classes to the rescue

The inline classes feature exists specifically for cases like this: when you need to wrap a primitive type but you want to avoid the performance overhead.

Let's see how you can use it:

inline class UserName(val value: String)
inline class UserPassword(val value: String)

fun safeLogin(username: UserName, password: UserPassword): Boolean {}

val inlineUsername = UserName("name")
val inlinePassword = UserPassword("asdasd")

safeLogin(inlineUsername, inlinePassword)

By applying the inline keyword on our wrapper classes we left marks for the compiler that we want to use those wrappers inlined at their call sites (similar to how we used inline functions.)

So after compilation, the performance requirement of these two lines is equal to the following code:

val inlineUsername = "name"
val inlinePassword = "asdasd"

But we get the benefit of type safety. E.g. if we write something like this it will not compile:

safeLogin(inlineUsername, "")

This neither:

safeLogin(inlinePassword, inlineUsername)

Great, isn't it? Now, let's dive into the details of how we can use this magical inline keyword.

Rules of inlining

Of course, you have to follow the rules to use such a great feature and there are a few for inline classes.

An inline class can only wrap one value as in our example:

inline class UserName(val value: String)

Other properties with backing fields are not allowed (basically anything that needs memory).

But we can define functions and computed properties without a problem.

inline class UserName(val value: String) {
    val firstLetter get() = value[0]
    fun toCapitalized() = value.capitalize()

Extension from other classes or to use an inline class as a supertype is also forbidden for obvious reasons.

Interesting aspects

There is an interesting mechanism regarding inline classes that we cannot pass without mentioning it. But to understand the solution first we have to realize that we can have a problem. Check this out:

Let's have an inline class wrapping an Int type:

inline class Minute(val m: Int)

And two functions somewhere in our application:

fun increase(i: Int) { }
fun increase(m: Minute) { }

Where is the problem? Same function name but different signatures. It should work, right? Unfortunately no. Let's think about how these functions will look like if they need to run on the JVM:

The first one is represented like this:

public final void increase(int m)

Because the Minute inline class is an int for the JVM it looks the same:

public final void increase(int i)

And that's a name collision that needs to be resolved. Here comes the solution called mangling.
What it does is simple. For functions using inline classes, a stable hashcode is added to their name like this:

public final void increase-kul7zYQ*(int m)

Problem solved!

This is, of course, a transparent behavior if we use functions like this from Kotlin. And because the - character is a forbidden symbol in Java, you will not be able to use it from that language.

Ready for an experiment?

The inline classes feature is in experimental state and that means JetBrains can introduce breaking changes to the implementation. So generally it's not recommended to decorate your classes with the inline keyword in production yet.
However, that shouldn't stop you from trying it in your pet project. It's great that we can peek into the future of the language so we should take this opportunity.
Also, this experiment is running for a long time now so I hope it will become a core feature of the language soon. I couldn't wait for it!