article banner (priority)

Effective Kotlin Item 49: Consider using inline value classes

This is a chapter from the book Effective Kotlin. You can find it on LeanPub or Amazon.

Not only functions can be inlined: objects holding a single value can also be replaced with this value. To do this, we need to define a class with a single read-only primary constructor property, a modifier value, and a JvmInline annotation.

@JvmInline value class Name(private val value: String) { // ... }

Value classes were introduced in Kotlin 1.5 due to Java’s plans to introduce value classes. Before that (but since Kotlin 1.3), we could use an inline modifier to achieve a similar result.

inline class Name(private val value: String) { // ... }

Such a class will be replaced with the value it holds whenever possible:

// Code val name: Name = Name("Marcin") // During compilation replaced with code similar to: val name: String = "Marcin"

Methods from such a class will be evaluated as static methods:

@JvmInline value class Name(private val value: String) { // ... fun greet() { print("Hello, I am $value") } } // Code val name: Name = Name("Marcin") name.greet() // During compilation replaced with code similar to: val name: String = "Marcin" Name.`greet-impl`(name)

We can use inline value classes to make a wrapper around some type (like String in the above example) with no performance overhead (Item 47: Avoid unnecessary object creation). Two especially popular uses of inline value classes are:

  • To indicate a unit of measure.

  • To use types to protect users from value misuse.

Let's discuss these separately.

Indicate unit of measure

Imagine that you need to use a method to set up a timer:

interface Timer { fun callAfter(time: Int, callback: () -> Unit) }

What is this time? It might be a time in milliseconds, seconds, or minutes; it is not clear at this point, so it is easy to make a mistake. A serious mistake. One famous example of such a mistake is the Mars Climate Orbiter, which plowed into the Martian atmosphere. The reason for this was that the software used to control it was developed by an external company, and it produced outputs in different measurement units than those expected by NASA. It produced results in pound-force seconds (lbf·s), while NASA expected newton-seconds (N·s). The total cost of the mission was 327.6 million USD, and it was a complete failure. As you can see, confusion of measurement units can be really expensive.

One common way for developers to suggest a measurement unit is by including it in the parameter name:

interface Timer { fun callAfter(timeMillis: Int, callback: () -> Unit) }

This is better, but it still leaves some space for mistakes. For example, the property name is often not visible when a function is used. Another problem is that indicating the type in this way is harder when the type is returned. In the example below, the time is returned from decideAboutTime but its measurement unit is not indicated at all. It might return the time in minutes, thus we will not set the time correctly.

interface User { fun decideAboutTime(): Int fun wakeUp() } interface Timer { fun callAfter(timeMillis: Int, callback: () -> Unit) } fun setUpUserWakeUpUser(user: User, timer: Timer) { val time: Int = user.decideAboutTime() timer.callAfter(time) { user.wakeUp() } }

We might introduce the measurement unit of the returned value in the function name, for instance by naming it decideAboutTimeMillis; however, this solution is not considered very good as it makes this function provide low-level information even when we don’t need it. Moreover, it does not necessarily solve the problem as a developer still needs to ensure that the measurement units match.

A better way to solve this problem is to introduce stricter types that will protect us from misusing types, and to make them efficient we can use inline value classes:

@JvmInline value class Minutes(val minutes: Int) { fun toMillis(): Millis = Millis(minutes * 60 * 1000) // ... } @JvmInline value class Millis(val milliseconds: Int) { // ... } interface User { fun decideAboutTime(): Minutes fun wakeUp() } interface Timer { fun callAfter(timeMillis: Millis, callback: () -> Unit) } fun setUpUserWakeUpUser(user: User, timer: Timer) { val time: Minutes = user.decideAboutTime() timer.callAfter(time) { // ERROR: Type mismatch user.wakeUp() } }

This would force us to use the correct type:

fun setUpUserWakeUpUser(user: User, timer: Timer) { val time = user.decideAboutTime() timer.callAfter(time.toMillis()) { user.wakeUp() } }

This is especially useful for metric units. For instance, on the frontend we often use a variety of units like pixels, millimeters, dp, etc. To support object creation, we can define DSL-like extension properties (you can make them inline as well):

inline val Int.min get() = Minutes(this) inline val Int.ms get() = Millis(this) val timeMin: Minutes = 10.min

Protect us from value misuse

In SQL databases, we often identify elements by their IDs, which are all just numbers. For instance, let’s say that you have a student’s grade in a system, which will probably need to reference the id of a student, teacher, school, etc:

@Entity(tableName = "grades") class Grades( @ColumnInfo(name = "studentId") val studentId: Int, @ColumnInfo(name = "teacherId") val teacherId: Int, @ColumnInfo(name = "schoolId") val schoolId: Int, // ... )

The problem is that it is really easy to later misuse all these ids, and the type system does not protect us because they are all of type Int. The solution is to wrap all these integers into separate inline value classes:

@JvmInline value class StudentId(val studentId: Int) @JvmInline value class TeacherId(val teacherId: Int) @JvmInline value class SchoolId(val studentId: Int) @Entity(tableName = "grades") class Grades( @ColumnInfo(name = "studentId") val studentId: StudentId, @ColumnInfo(name = "teacherId") val teacherId: TeacherId, @ColumnInfo(name = "schoolId") val schoolId: SchoolId, // ... )

Now those id uses will be safe and the database will be generated correctly because all these types will be replaced with Int anyway during compilation. This way, inline value classes allow us to introduce types where they were not allowed before; thanks to this, we have safer code with no performance overhead.

Inline value classes and interfaces

Inline value classes can implement interfaces. We could use this in the example presented above to avoid casting from one class to another.

interface TimeUnit { val millis: Long } @JvmInline value class Minutes(val minutes: Long) : TimeUnit { override val millis: Long get() = minutes * 60 * 1000 // ... } @JvmInline value class Millis(val milliseconds: Long) : TimeUnit { override val millis: Long get() = milliseconds } // the type under the hood is TimeUnit fun setUpTimer(time: TimeUnit) { val millis = time.millis //... } setUpTimer(Minutes(123)) setUpTimer(Millis(456789))

The catch is that when an object is used through an interface, it cannot be inlined. Therefore, in the above example, there is no advantage to using inline value classes since wrapped objects need to be created to let us present a type through this interface. When we present inline value classes through an interface, such classes are not inlined.

Another situation in which a type will not be inlined is when it is nullable and the value class holds a primitive as a parameter. In the example below, when Millis is used as a parameter type, it will be replaced with Long. However, if Millis? is used, it cannot be replaced because Long cannot be null. But if Millis held a non-primitive type, like String, then its type nullability wouldn't influence inlining.

@JvmInline value class Millis(val milliseconds: Long) { val millis: Long get() = milliseconds } // the type under the hood is @Nullable Millis fun setUpTimer(time: Millis?) { val millis = time?.millis //... } // the type under the hood is long fun setUpTimer(time: Millis) { val millis = time.millis //... } fun main() { setUpTimer(Millis(456789)) }

Typealias

Kotlin’s typealias lets us create another name for a type:

typealias NewName = Int val n: NewName = 10

Naming types is a useful capability that is used especially when we deal with long and repeatable types. For instance, it is a popular practice to name repeatable function types:

typealias ClickListener = (view: View, event: Event) -> Unit class View { fun addClickListener(listener: ClickListener) {} fun removeClickListener(listener: ClickListener) {} //... }

What needs to be understood though is that type aliases do not protect us in any way from type misuse. They just add a new name for a type. If we named Int as both Millis and Seconds, we would create the illusion that the type system protects us, but it does not:

typealias Seconds = Int typealias Millis = Int fun getTime(): Millis = 10 fun setUpTimer(time: Seconds) {} fun main() { val seconds: Seconds = 10 val millis: Millis = seconds // No compiler error setUpTimer(getTime()) }

In the above example, it would be easier to find what is wrong without using type aliases. This is why they should not be used this way. To indicate a unit of measure, use a parameter name or classes: a name is cheaper, but classes give better safety. When we use inline value classes, we take the best from both options: they are both cheap and safe.

Summary

Inline value classes let us wrap a type without a performance overhead. Therefore, we improve safety by making our type system protect us from value misuse. If you use a type whose meaning is unclear (like Int or String), especially a type that might have different units of measure, consider wrapping it with inline value classes.