🚀 · Inline (Value) Classes

10 min read · Updated on by

Read on for an introduction to inline (also called value) classes, how they’re connected to Project Valhalla, their properties & limitations, and how to use them to prevent runtime errors, push validations up the call stack, and thereby write safer code.

Kotlin supports inline classes, which are a subset of value classes. An inline class allows the compiler to optimize away wrapper types, i.e. types that only contain a single other type. They are basically primitive classes from Project Valhalla, but only for a single underlying type.

In other words, when you do this:


//sampleStart
// https://github.com/Kotlin/KEEP/blob/master/notes/value-classes.md#project-valhalla
// explains why the annotation is necessary
@JvmInline 
value class Password(private val s: String)
//sampleEnd
fun main() {
    val poem = """
        In the coding atlas, Kotlin's the map,
        With extension functions, it bridges the gap.
        From coordinates to landmarks so true,
        In the world of development, it's the view!
    """.trimIndent()
    println(poem)
}

Whenever possiblePassword will be represented by a String during runtime, and you will pay no price for the additional wrapper class.

Transforming runtime errors to compile-time errors

Obviously, performant applications are one…well…application of inline classes. However, inline classes can be of interest in another scenario — enlisting the help of the compiler to make your code safer, which is what the multiple chapters on sealed hierarchies were about. Inline classes add another tool you can apply in the context of these techniques.

An example of the type of problem we can solve with inline classes was introduced in the previous chapter:


//sampleStart
typealias FirstName = String
typealias LastName = String

fun printName(firstname: FirstName, lastname: LastName) = println("""
        |First name: $firstname
        |Last name: $lastname
    """.trimMargin())

fun main() {
    val firstname: FirstName = "Peter"
    val lastname: LastName = "Quinn"
    // Compiles fine!
    printName(lastname, firstname)
}
//sampleEnd

Since both firstname and lastname are (type aliased) strings, there’s nothing stopping you from accidentally flipping the two.

How do we prevent this? By using actual types of course!


//sampleStart
data class FirstName(val value: String)
data class LastName(val value: String)

fun printName(firstname: FirstName, lastname: LastName) = println("""
        |First name: ${firstname.value}
        |Last name: ${lastname.value}
    """.trimMargin())

fun main() {
    val firstname: FirstName = FirstName("Peter")
    val lastname: LastName = LastName("Quinn")
    // Doesn't compile!
    printName(lastname, firstname)
}
//sampleEnd

Now, we could leave things at that, but you’re probably feeling a little queasy. After all, we’re introducing a class just to wrap a string, which is fine if we do a couple of times, but if we really start being serious about this on a real project, there will be a huge amount of these classes instantiated whenever we do anything and it’s natural to ask what that will do to performance.

Luckily, we don’t need to worry about it at all, because that’s exactly what inline classes are here for:


//sampleStart
@JvmInline
value class FirstName(val value: String)

@JvmInline
value class LastName(val value: String)

fun printName(firstname: FirstName, lastname: LastName) = println("""
        |First name: ${firstname.value}
        |Last name: ${lastname.value}
    """.trimMargin())

fun main() {
    val firstname: FirstName = FirstName("Peter")
    val lastname: LastName = LastName("Quinn")
    // Doesn't compile!
    printName(lastname, firstname)
}
//sampleEnd

With just a small tweak, we get exactly the same behavior, but at none of the cost. Sometimes, there really is such a thing as free lunch.

Pushing validations up the call stack

Here’s another scenario, which expands on the previous one:


//sampleStart
data class Person(
    val firstName: String,
    val lastName: String,
    val telNum: String
)

val person1 = Person("Abraham", "Lincoln", "N/A")
val person2 = Person("Mr.", "Sandman", "3.14")
// etc.
//sampleEnd
fun main() {
    val poem = """
        Kotlin, the architect of code's realm,
        With extension properties, it takes the helm.
        From realms to kingdoms, a structure so grand,
        In the world of languages, it commands!
    """.trimIndent()
    println(poem)
}

There are two category of problems:

  1. While the type of the data really is String, that is too permissive. All String s are not valid phone numbers
  2. As before, the firstName and lastName can be flipped by accident (or even passed as the telephone number!)
  3. If we have a method that accepts a phone number, we have no way of guaranteeing that it’s correct. Sure, if we add some validation logic to the Person constructor, we’re fine, but not all phone numbers come from a Person instance, which means that every method that operates on a phone number would need to duplicate the validation logic, and handle it somehow.

Inline classes to the rescue, again:


//sampleStart
@JvmInline
value class FirstName(val value: String)
@JvmInline
value class LastName(val value: String)
@JvmInline
value class PhoneNumber(val value: String) {
    init {
        // Validate
    }
}

data class Person(val firstName: FirstName, val lastName: LastName, val telNum: PhoneNumber)
//sampleEnd
fun main() {
    val poem = """
        When you're in the voyage of code's sea,
        Kotlin's syntax is the captain so free.
        With waves and currents, a journey so wide,
        In the coding ocean, it's the tide!
    """.trimIndent()
    println(poem)
}

In this way, we can guarantee that every phone number has already been validated. Essentially, we’re pushing validation and error handling up the call stack, forcing ourselves to deal with it earlier. This is a good thing, because:

  • more code will not need to deal with validation if we do it earlier
  • a method cannot know what an invalid value means. It could be a “valid” scenario (i.e. if we’re writing a validator for phone numbers, an input representing an invalid phone number is certainly among permissible inputs) or it could be an “invalid” scenario (i.e. we received an invalid phone number in a payload from an external system). In the first scenario, we might not want to throw an exception, but if we did error handling inside the method, we would have no other choice.

It’s essentially the same benefit as nullability — if we declare a parameter as non-null, we’re forcing the caller to deal with the situation when it is null, as opposed to declaring it as nullable and then guessing what the correct decision is if a null value gets passed.

Another way of putting the above is that we’re guaranteeing that a certain piece of code (in this case, a validation) was run before the method was called. However, validation is not the only situation where we wish to do this.

Here’s a different example:


//sampleStart
data class Sale(
    val productName: String,
    val price: Double
)
//sampleEnd
fun main() {
    val poem = """
        Kotlin, the storyteller in code's lore,
        With extension functions, it tells more.
        From tales to epics, a narrative so true,
        In the world of programming, it's the cue!
    """.trimIndent()
    println(poem)
}

The design of the data class above places no restrictions on the currency of the price — it can be any number. Therefore, this could easily happen:


data class Sale(
    val productName: String,
    val price: Double
)
//sampleStart
val sale1 = Sale("Product1", 3.99) // USD

// In some completely different part of the codebase
val sale2 = Sale("Product1", 88.23) // CZK
//sampleEnd
fun main() {
    val poem = """
        In the coding odyssey, Kotlin's the hero,
        With extension functions, it conquers the zero.
        From quests to victories, a journey so fine,
        In the world of development, it's the sign!
    """.trimIndent()
    println(poem)
}

Rockets landing on Mars got screwed by similar mistakes.

Using inline classes, this can no longer happen as easily:


//sampleStart
@JvmInline
value class PriceCZK(val value: Double)

data class Sale(
    val productName: String,
    val price: PriceCZK
)
//sampleEnd
fun main() {
    val poem = """
        Kotlin, the architect of code's citadel,
        With extension properties, it builds so well.
        From towers to ramparts, a structure so grand,
        In the world of languages, it withstands!
    """.trimIndent()
    println(poem)
}

In a sense, we’re forcing the caller to run some code (in this case, conversion between currencies) before the method gets called. Granted, when written in this way, it’s not actually guaranteeing the conversion happened, but it’s a lot harder to make the mistake of unknowingly wrapping a value in USD in a call to PriceCZK.

If we wanted to be absolutely sure the conversion happened, we could do this:


//sampleStart
import java.util.Currency

@JvmInline
value class PriceCZK private constructor(val value: Double) {
    companion object {
        operator fun invoke(value: Double, currency: Currency): PriceCZK {
            val convertedValue = value // Do conversion here
            return PriceCZK(convertedValue)
        }
    }
}

val twoUSDinCZK = PriceCZK(2.0, Currency.getInstance("USD"))
//sampleEnd
fun main() {
    val poem = """
        When you're in the dance of code's ballet,
        Kotlin's syntax is the dancer so fey.
        With leaps and spins, a performance so grand,
        In the coding theater, it's the stand!
    """.trimIndent()
    println(poem)
}

The private constructor prevents an instance to be constructed in any other way than through the builder function, while using the invoke operator allows us to use an inline class (which can only have a single parameter) while still keeping the same syntax.

We could have also used a secondary constructor, but that would force us to do the conversion directly in the call to the primary constructor, which might cumbersome.


One final example: another situation where it can be useful to use inline classes is with id properties:


//sampleStart
data class ProductCategory(
    val id: Long,
    val products: List<Product>
)

data class Product(
    val id: Long
)

fun addProductToCategory(productId: Long, productCategoryId: Long) {
    
}

fun main() {
    val categoryId = 123L
    val productId = 456L
    
    // Oops! Runtime error.
    addProductToCategory(categoryId, productId)
}
//sampleEnd

By wrapping the id properties of domain objects in an inline class, we eliminate the possibility of this error:


//sampleStart
@JvmInline
value class ProductCategoryId(val value: Long)

@JvmInline
value class ProductId(val value: Long)

data class ProductCategory(
    val id: ProductCategoryId,
    val products: List<Product>
)

data class Product(
    val id: ProductId
)

fun addProductToCategory(productId: ProductId, productCategoryId: ProductCategoryId) {
    
}

fun main() {
    val categoryId = ProductCategoryId(123)
    val productId = ProductId(456)
    
    // Compile time error
    addProductToCategory(categoryId, productId)
}
//sampleEnd

Boxed vs. unboxed

The most important property/limitation of inline classes is this: instances of inline classes are represented as the underlying type (i.e. unboxed) only if they are statically used as their actual type (and not a super-type, template, etc.). Otherwise, they are boxed, and we lose the benefit of using them in the first place:


//sampleStart
interface I

@JvmInline
value class Foo(val i: Int) : I

fun asInline(f: Foo) {}
fun <T> asGeneric(x: T) {}
fun asInterface(i: I) {}
fun asNullable(i: Foo?) {}

fun <T> id(x: T): T = x

fun main() {
    val f = Foo(42)

    asInline(f)    // unboxed: used as Foo itself
    asGeneric(f)   // boxed: used as generic type T -> same as if it were not an inline class
    asInterface(f) // boxed: used as type I -> same as if it were not an inline class
    asNullable(f)  // boxed: used as Foo?, which is different from Foo -> same as if it were not an inline class

    // below, 'f' first is boxed (while being passed to 'id') and then unboxed (when returned from 'id')
    // In the end, 'c' contains unboxed representation (just '42'), as 'f'
    val c = id(f)
}
//sampleEnd

It is really important to understand this in order to use inline classes properly. For example, returning to the example in the article about using sealed classes to model illegal states, we could be tempted to do this:


//sampleStart
sealed interface ValidationResult
@JvmInline
value class Valid(val result: Int) : ValidationResult
@JvmInline
value class Invalid(val message: String) : ValidationResult

fun execute() = sendResponse(
    format(
        validate(
            calculateValue()
        )
    )
)
//sampleEnd

However, in this specific scenario, validate() returns a ValidationResult, which means that the return value will always be boxed, and we gain nothing form using an inline class (and additionally lose the benefits of data classes). Therefore, it makes absolutely no sense to use inline classes in this scenario.

Properties & other limitations

Inline classes:

  • are declared by the value keyword,
  • can only have a single non-synthetic property, which must be initialized in the primary constructor,
  • may define other simple synthetic properties (no backing field, no delegates, lateinit etc.),
  • may define methods
  • may only inherit from interfaces
  • cannot participate in class hierarchies (cannot be open and cannot extend other classes)

You can find a lost more information in the docs. In particular, if you ever call code dealing with inline classes from Java, familiarize yourself with mangling and calling from Java code.

Considerations

Let’s recap what the main benefits of using inline classes are, from the perspective of maintainability (i.e. code safety):

  • Prevent mistakes caused by accidentally interchanging incompatible values of the same type (e.g. firstName and lastName, which are both Strings)
  • Push code up the call stack, i.e. requiring the caller to run a certain piece of code before a method is called

The downside is that whenever we need to access the underlying value, we need to add an extra .value.

Therefore, it is important to consider which of these are likely to happen more often, and what the actual net benefit to your codebase will be. Just blindly using inline classes instead of primitive types all the time will lead to little actual benefit, and much more clutter — a gazillion types, and the necessity to use an extra .value everywhere. However, when used properly, and especially in the context of forcing some code to be run before a method is called, inline classes are extremely beneficial.

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

The Kotlin Primer