The provided content discusses the use of inline classes in Kotlin, detailing their properties, benefits for code safety and performance, and their limitations.
Abstract
Inline classes in Kotlin are a feature that allows for compiler optimizations by eliminating unnecessary wrapper types, aligning with Project Valhalla's goals. They ensure runtime representation as a single underlying type, thus preventing errors and enhancing code safety. The article explains how inline classes can transform runtime errors into compile-time errors, push code up the call stack, and enforce certain checks or conversions before method execution, thereby reducing the need for repetitive validation logic. The content also covers scenarios where inline classes are beneficial, such as in domain modeling and preventing illegal states, and outlines their limitations, such as boxing behavior under certain conditions and restrictions in their declaration and usage. The article emphasizes the importance of understanding when and how to use inline classes effectively to avoid unnecessary complexity and to leverage their potential for writing safer and more efficient code.
Opinions
Inline classes are seen as a powerful tool for improving application performance by optimizing away wrapper types.
The author believes that inline classes contribute to code safety by enabling the compiler to catch errors early in the development process.
There is an emphasis on the importance of using inline classes to push validation and error handling up the call stack, ensuring that certain checks are performed before a method is called.
The article suggests that inline classes can help prevent catastrophic errors, such as those that have historically occurred in software development (e.g., the Mars Climate Orbiter failure due to unit conversion errors).
The author expresses that while inline classes have significant benefits, they should be used judiciously to avoid cluttering the codebase with unnecessary types and to prevent the overuse of the .value property accessor.
The content conveys that a deep understanding of inline classes' behavior, especially regarding boxing and unboxing, is crucial for their effective application in Kotlin projects.
The author provides a pragmatic view on the trade-offs of using inline classes, highlighting that their benefits in maintainability and code safety must be weighed against the potential for increased code complexity.
Inline (Value) Classes
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 code up the call stack, and thereby write safer code.
— — — — — — — — — — — — — — —
THE CURRENT VERSION OF THIS ARTICLE IS PUBLISHED HERE.
This article is part of the Kotlin Primer, an opinionated guide to the Kotlin language, which is indented to help facilitate Kotlin adoption inside Java-centric organizations. It was originally written as an organizational learning resource for Etnetera a.s. and I would like to express my sincere gratitude for their support.
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:
Whenever possible, Password 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 multiplechapters on sealedhierarchies 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:
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!
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:
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 Code Up the Call Stack
Here’s another scenario, which expands on the previous one:
There are two category of problems:
While the type of the data really is String, that is too permissive. All String s are not valid phone numbers
As before, the firstName and lastName can be flipped by accident (or even passed as the telephone number!)
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:
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:
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:
Using inline classes, this can no longer happen as easily:
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:
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:
By wrapping the id properties of domain objects in an inline class, we eliminate the possibility of this error:
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:
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:
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.