Value Classes in Kotlin: Good-Bye, Type Aliases!?

With the release of Kotlin 1.5.0, value classes (formerly known as inline classes) are finally stable and were released from their

@OptIn
@OptIn annotations. Many were hyped about the release, but it also created a lot of confusion as we now have three very similar tools in Kotlin: Typealises, data classes, and value classes. So which one shall we use now? Can we drop our type aliases and data classes altogether and replace them all with value classes? That’s what we’re going to explore in this article.


The Problem

Classes in Kotlin solve two problems:

  1. They convey meaning through their name and make it easier for us to understand what kind of object is passed along.
  2. They enforce type-safety by making sure that an object of class A cannot be passed to a function that expects an object of class B as an input parameter. This prevents serious bugs at compile-time.

Primitive types like

Int
Int,
Boolean
Boolean or
Double
Double also enforce type-safety (you can’t just pass a
Double
Double where a
Boolean
Boolean is expected), but they don’t really convey a meaning (other than an object being a number of a certain format, for example).

A

Double
Double could be pretty much anything: a temperature in degrees Celsius, a weight in kilograms, or your screen’s brightness level in percent. All we know is that we’re dealing with a floating-point number with double precision (64 bits), but it doesn’t tell us what this number represents. For that reason, the semantic type-safety is lost:

If we have a function to set your display’s brightness level:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
fun setDisplayBrightness(newDisplayBrightness: Double) { ... }
fun setDisplayBrightness(newDisplayBrightness: Double) { ... }
fun setDisplayBrightness(newDisplayBrightness: Double) { ... }

we can call this function with any

Double
Double value we want and might accidentally pass a number with a completely different meaning:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
val weight: Double = 85.4
setDisplayBrightness(weight) // 💥
val weight: Double = 85.4 setDisplayBrightness(weight) // 💥
val weight: Double = 85.4
setDisplayBrightness(weight) // 💥

This programming error cannot be detected at compile-time and will most likely lead to a crash at runtime – or much worse: to unexpected behavior.


The Solution

There are different approaches to solving the two problems mentioned above. We could just wrap a primitive type in a class, but that comes with a lot of overhead. So let’s see how we can tackle these problems above with

  • data classes,
  • type aliases,
  • and value classes

and explore which of these tools is the best for this purpose!

1st Attempt: Data Classes

The easiest solution (present since the dawn of Kotlin) would be a data class:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
data class DisplayBrightness(val value: Double)
fun setDisplayBrightness(newDisplayBrightness: DisplayBrightness) { ... }
data class DisplayBrightness(val value: Double) fun setDisplayBrightness(newDisplayBrightness: DisplayBrightness) { ... }
data class DisplayBrightness(val value: Double)

fun setDisplayBrightness(newDisplayBrightness: DisplayBrightness) { ... }

✅ Benefits

DisplayBrightness
DisplayBrightness is thus a type of its own, that holds a
Double
Double value, but is assignment-incompatible with
Double
Double (i.e.
setDisplayBrightness(DisplayBrightness(0.5)
setDisplayBrightness(DisplayBrightness(0.5)) would work while
setDisplayBrightness(0.5)
setDisplayBrightness(0.5) would cause a compiler error). While this would still allow the caller to do
setDisplayBrightness(DisplayBrightness(person.weight))
setDisplayBrightness(DisplayBrightness(person.weight)), it makes it much more obvious that there’s something fishy going on.

⛔️ Disadvantages

However, there’s one big disadvantage: Instantiating data classes is expensive. Primitive values can be written to the stack which is fast and efficient. Instances of data classes are written to the heap, which takes more time and memory.
How much more time, you ask? Let’s test it:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
data class DisplayBrightnessDataClass(val value: Double)
@OptIn(ExperimentalTime::class)
fun main(){
val dataClassTime = measureTime {
repeat(1000000000) { DisplayBrightnessDataClass(0.5) }
}
println("Data classes took ${dataClassTime.toDouble(MILLISECONDS)} ms")
val primitiveTime = measureTime {
repeat(1000000000) { var brightness = 0.5 }
}
println("Primitive types took ${primitiveTime.toDouble(MILLISECONDS)} ms")
}
data class DisplayBrightnessDataClass(val value: Double) @OptIn(ExperimentalTime::class) fun main(){ val dataClassTime = measureTime { repeat(1000000000) { DisplayBrightnessDataClass(0.5) } } println("Data classes took ${dataClassTime.toDouble(MILLISECONDS)} ms") val primitiveTime = measureTime { repeat(1000000000) { var brightness = 0.5 } } println("Primitive types took ${primitiveTime.toDouble(MILLISECONDS)} ms") }
data class DisplayBrightnessDataClass(val value: Double)

@OptIn(ExperimentalTime::class)
fun main(){
    val dataClassTime = measureTime {
        repeat(1000000000) { DisplayBrightnessDataClass(0.5) }
    }
    println("Data classes took ${dataClassTime.toDouble(MILLISECONDS)} ms")

    val primitiveTime = measureTime {
        repeat(1000000000) { var brightness = 0.5 }
    }
    println("Primitive types took ${primitiveTime.toDouble(MILLISECONDS)} ms")
}

…leads to this output:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
Data classes took 9.898582 ms
Primitive types took 2.812561 ms
Data classes took 9.898582 ms Primitive types took 2.812561 ms
Data classes took 9.898582 ms
Primitive types took 2.812561 ms

While this performance hit might seem marginal at first, given that modern computers are really really fast, such small improvements end up making a big difference in performance-critical applications.

2nd Attempt: Type Aliases

Type aliases basically give a type a second name, like so:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
typealias DisplayBrightness = Double
fun setDisplayBrightness(newDisplayBrightness: DisplayBrightness) { ... }
typealias DisplayBrightness = Double fun setDisplayBrightness(newDisplayBrightness: DisplayBrightness) { ... }
typealias DisplayBrightness = Double

fun setDisplayBrightness(newDisplayBrightness: DisplayBrightness) { ... }

✅ Benefits

Under the hood,

Double
Double and
DisplayBrightness
DisplayBrightness are synonyms.
Whenever the compiler sees
DisplayBrightness
DisplayBrightness, it basically replaces it with
Double
Double and moves on.
Since our new
DisplayBrightness
DisplayBrightness type alias is now equal to Double, it also receives the same optimization treatment as Double, resulting in equally fast speeds.
If we expand the test from earlier, we can see that the type alias and primitive type take approximately the same time to execute:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
Data classes took 7.743406 ms
Primitive types took 2.77597 ms
Type aliases took 2.688276 ms
Data classes took 7.743406 ms Primitive types took 2.77597 ms Type aliases took 2.688276 ms
Data classes took 7.743406 ms
Primitive types took 2.77597 ms
Type aliases took 2.688276 ms

Since

DisplayBrightness
DisplayBrightness and
Double
Double become synonyms here, all operations that support
Double
Double, will also accept
DisplayBrightness
DisplayBrightness:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
val first: DisplayBrightness = 0.5
val second: DisplayBrightness = 0.1
val summedBrightness = first + second // 0.6
first.isNaN() // false
val first: DisplayBrightness = 0.5 val second: DisplayBrightness = 0.1 val summedBrightness = first + second // 0.6 first.isNaN() // false
val first: DisplayBrightness = 0.5
val second: DisplayBrightness = 0.1
val summedBrightness = first + second // 0.6
first.isNaN() // false

⛔️ Disadvantages

The downside of this is that

DisplayBrightness
DisplayBrightness and
Double
Double are even assignment-compatible, meaning that the compiler will happily accept this:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
typealias DisplayBrightness = Double
typealias Weight = Double
fun setDisplayBrightness(newDisplayBrightness: DisplayBrightness) { ... }
fun callingFunction() {
val weight: Weight = 85.4
setDisplayBrightness(weight)
}
typealias DisplayBrightness = Double typealias Weight = Double fun setDisplayBrightness(newDisplayBrightness: DisplayBrightness) { ... } fun callingFunction() { val weight: Weight = 85.4 setDisplayBrightness(weight) }
typealias DisplayBrightness = Double
typealias Weight = Double

fun setDisplayBrightness(newDisplayBrightness: DisplayBrightness) { ... }

fun callingFunction() {
    val weight: Weight = 85.4
    setDisplayBrightness(weight)
}

So, did we really fix our problem? Well, only half of it. While type aliases make function signatures more explicit and are much faster than data classes, the fact that

DisplayBrightness
DisplayBrightness and
Double
Double are also assignment-compatible leaves the type-safety problem unsolved.

3rd Attempt: Value Classes

At first glance, value classes might look pretty similar to data classes. The signature looks exactly the same, except that instead of

data class
data class the keyword is
value class
value class:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
@JvmInline
value class DisplayBrightness(val value: Double)
fun setDisplayBrightness(newDisplayBrightness: DisplayBrightness) { ... }
@JvmInline value class DisplayBrightness(val value: Double) fun setDisplayBrightness(newDisplayBrightness: DisplayBrightness) { ... }
@JvmInline
value class DisplayBrightness(val value: Double)

fun setDisplayBrightness(newDisplayBrightness: DisplayBrightness) { ... }

Also, you might have noticed the

@JvmInline
@JvmInline annotation. The KEEP about value classes explains this and the reason why value classes can currently only have 1 field.

ℹ️ Why

@JmvInline
@JmvInline is required:
In short, while the Kotlin/Native and Kotlin/JS backends would technically support value classes with more than one field, Kotlin/JVM currently does not. This is because the JVM only supports its built-in primitive types. There are however plans for the so-called project Valhalla (see the corresponding JEP), which would allow user-defined primitive types. As things are standing now, the Kotlin team deems project Valhalla to be the best compilation strategy for value classes. However, project Valhalla is far from being stable, so they needed to find a temporary compilation strategy that can be employed now. In order to make this obvious,
@JvmInline
@JvmInline is enforced for the time being.

✅ Benefits

Behind the scenes, the compiler treats value classes like a type alias – with one major difference: Value classes are not assignment-compatible, meaning that this code will not compile:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
@JvmInline
value class DisplayBrightness(val value: Double)
fun setDisplayBrightness(newDisplayBrightness: DisplayBrightness) { ... }
fun callingFunction() {
val weight: Double = 85.4
setDisplayBrightness(weight) // 💥
}
@JvmInline value class DisplayBrightness(val value: Double) fun setDisplayBrightness(newDisplayBrightness: DisplayBrightness) { ... } fun callingFunction() { val weight: Double = 85.4 setDisplayBrightness(weight) // 💥 }
@JvmInline
value class DisplayBrightness(val value: Double)

fun setDisplayBrightness(newDisplayBrightness: DisplayBrightness) { ... }

fun callingFunction() {
    val weight: Double = 85.4
    setDisplayBrightness(weight) // 💥
}

With the performance test from above extended, we can see that value classes have an equally fast performance as primitive types and thus type aliases:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
Data classes took 7.268809 ms
Primitive types took 2.799518 ms
Type aliases took 2.627111 ms
Value classes took 2.883411 ms
Data classes took 7.268809 ms Primitive types took 2.799518 ms Type aliases took 2.627111 ms Value classes took 2.883411 ms
Data classes took 7.268809 ms
Primitive types took 2.799518 ms
Type aliases took 2.627111 ms
Value classes took 2.883411 ms

Should I always use value classes?

So, it seems like value classes do check all the boxes, right? They…

  • make variable declarations and function signatures more explicit, ✅
  • keep the efficiency of primitive types, ✅
  • are not assignment-compatible with their underlying type, preventing the caller from doing stupid things, ✅
  • and support many features of data classes, like constructors,
    init
    init, functions and even additional properties (but only with getters). ✅

The only remaining use-case for a data class is when you need to wrap multiple parameters. Value classes are limited to one parameter in their constructor for the time being.

Similarly, type aliases still have use cases that cannot be covered by value classes (or do not align with their purpose):

  1. the abbreviation of long signatures of generic types:
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
typealias Restaurant = Organization<(Currency, Coupon?) -> Sustenance
typealias Restaurant = Organization<(Currency, Coupon?) -> Sustenance
typealias Restaurant = Organization<(Currency, Coupon?) -> Sustenance
  1. parameters of higher-order functions:
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
typealias ListReducer<T> = (List<T>, List<T>) -> List<T>
typealias ListReducer<T> = (List<T>, List<T>) -> List<T>
typealias ListReducer<T> = (List<T>, List<T>) -> List<T>

Except for those exceptions, value classes really are the best solution for most cases. (For that reason, we are currently moving our projects to value classes.)


Going Further

There are two documents that really helped us understand how value classes work and what engineering thoughts went into the design process:

The KEEP also talks about possible future developments and design ideas. This article on typealias.com explains how type aliases work and how they should be used – a recommended read.

As a closing note: if your company needs a developer team to implement an iOS or Android app, reach out to us at contact@quickbirdstudios.com.

If you’re interested in Kotlin language engineering in general, you might like our blog article Kotlin’s Sealed Interfaces & The Hole in The Sealing.

Share via
Copy link
Powered by Social Snap

Get notified when our next article is born!

(no spam, just one app-development-related article
per month)