article banner (priority)

Effective Kotlin Item 52: 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.

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.

  • To optimize for memory usage.

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

Regarding indicating the amount of time, we can also use the Duration class from the standard library, which is an inline value class, and that offers DSL-like extension properties:

val time: Duration = 10.minutes

Protect us from value misuse

It is a popular practice in bigger projects to use a wrapper around primitive types to protect us from misusing them. For instance, let's say that you write an application for a university in which each student is identified by a unique ID but is also associated with a class id. Both might be represented as raw strings, but it would be easy to make a mistake and use the class id instead of the student id. To avoid this, we should define wrappers over different kinds of ids, and we make these inline value classes to avoid performance overhead:

@JvmInline value class StudentId(val value: String) @JvmInline value class ClassId(val value: String) data class Student(val id: StudentId, val classId: ClassId)

Optimize for memory usage

As we learned in Item 47: Avoid unnecessary object creation, using primitive types instead of wrapped types is an optimization. However, operating on primitives can be harder. To have your cake and eat it too, we can use inline value classes to wrap primitives and operate on them as if they were objects:

@JvmInline value class OptionalDouble(val value: Double) { fun isUndefined() = value.isNaN() companion object { const val UNDEFINED_VALUE = Double.NaN val Undefined = OptionalDouble(UNDEFINED_VALUE) } }

Inline value classes and interfaces

Inline value classes can implement interfaces. We could use this in the example presented above to avoid transforming from one type 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.