Can you dynamically instantiate value classes by type?

Inline value classes are really useful as type-safe identifiers. However, sometimes you need to implement a generic conversion between the wrapper, and the naked type (which is actually conceptually no-op, when the wrapper is inlined and not boxed).

Some examples include - marshalling such IDs to/from json, (de)-serialization to database etc.

The conversion from wrapper to naked type is easy. Just let wrappers implement some common interface that exposes the value of the wrapper.

interface Id<T : Serializable> {
	val id: T
}

@JvmInline
value class UserId(override val id: String) : Id<String>

To go from wrapper to value, just access the id value.

But how do you convert the value to the wrapper?

You cannot even use introspection, which you could if the wrapper would be implemented as a real wrapper. Then you could instantiate the wrapper using introspection, if the type of the wrapper is known. But if you want to convert a value to unboxed wrapper, no instantiation actually happens, does it?

Is there any way around this problem? The only workaround I see is to create a nasty switch where essentially all known wrapper types need to be registered:

@Suppress("UNCHECKED_CAST")
fun <T : Serializable, I : Id<T>> wrap(type : KClass<I>, value : T): I = when (type) {
	UserId::class -> UserId(value as String) as I
	ProjectId::class -> ProjectId(value as String) as I
	else -> throw IllegalArgumentException("Unsupported type $type")
}

Is there some way to automate this conversion?

I also do not understand how the unboxed state of an instance actually works, when any instance, even in a context in which it can be unboxed, knows it correct, i.e. wrapper, type, as specified in documentation: Inline value classes | Kotlin Documentation

If I modify the code as:

fun asInline(f: Foo) {
    println(f)
}

it prints Foo(i=42), how could this work, without invoking the actual constructor of the type Foo?

Maybe some reification magic happens here, which allows calling wrapper’s methods with naked instance, including .toString and .javaType.

1 Like

I find your question a little confusing.

Why not?

fun main() {
    val id = wrap(UserId::class, "123")
    println(id) // UserId(id=123)
}

fun <T, I : Id<T>> wrap(type : KClass<I>, value : T): I = type.primaryConstructor!!.call(value)

What do you mean that you want to convert to unboxed wrapper? Unboxed wrapper is the value itself. We can’t use unboxed values in generic context or in any place where we don’t use the exact UserId type. This is how value classes work - they are unboxed only in places where we know the exact type.

It calls the constructor of Foo - it has to box the value before passing Foo into a place where it is not typed as Foo directly (in this case: Any).

If your case is that you have a class like:

data class User(var id: UserId)

and you would like to instantiate it or set its prop by using reflection and passing the String directly (because we know User internally holds a String), then I believe Kotlin will make it hard on you. Reflection verifies we don’t pass just any String, but UserId specifically, so without some nasty hacks we can’t avoid boxing+unboxing. If we instantiate User from a regular code, we avoid boxing+unboxing, because we verify string is a UserId at the compile time.

1 Like

And what is the point of calling wrap(UserId::class, "123")? If you know that the string is a user ID, then just use UserId("123").

1 Like

println accepts Any as a parameter, so the value class gets boxed. In general, the value class is boxed each time something other than the compiler needs to know about it.

Thanks for replies. Yes, that makes sense. The actual behavior was not clear to me from the documentation. I thought that boxing is necessary when you deal with generics, but it also takes place when you simply upcast the instance to a super-type (interface). Or Any, of course.

But that clarified, it seems to me that the value classes are not that great after all. Because if you work with them using their super type, the boxing and unboxing happens all the time, which pretty much kills the whole point of this optimization, it could actually be a lot worse, you do boxing/unboxing every time you cross the special/general barrier of the type use.

Another drawback which is pretty obvious, but not much clarified in the doc, is that value classes are quite unusable when you need Java interoperability. Then you need to work with the naked values. At least when kotlin function returns value class.

So if I call:

fun getUserId() : UserId

from Java, I will see String, not UserId, is that right?

I assume that boxing also takes place when value instances are put to collections. So if I create

val list : MutableList<UserId> = mutableListOf(userId1, userId2)

they get boxed/unboxed every time they are inserted/retrieved from/to list?

What do you mean that you want to convert to unboxed wrapper? Unboxed wrapper is the value itself. We can’t use unboxed values in generic context or in any place where we don’t use the exact UserId type. This is how value classes work - they are unboxed only in places where we know the exact type.

By unboxed wrapper I mean something like this:

fun <T, I : Id<T>> wrap(type : KClass<I>, value : T): I = type.primaryConstructor!!.call(value)

val userId : UserId = wrap(UserId::class, "someId")

userList += userId

so the second statement creates a boxed wrapper, and must immediately unwrap it to put it to variable of type UserId (which I call unboxed wrapper, internally it is the naked value). The third statement must wrap it again. Of course the second statement may be simple UserId("someId"), I just simulated some more complex process, as Json deserialization.

It really works similarly to types like Int, which are managed automatically. Problem is that you have no control over this process. In Java, you could prefer using object representation of an integer in some context, to reduce boxing and unboxing.

I think the main misconception was that value classes (almost) guarantee to be inlined. Value class is still a class and this is how we should think of it. By making it value we allow the compiler to perform some optimizations on a best-effort basis. Documentation doesn’t explain all possible cases, because most of the time it shouldn’t matter for the developer - this is an internal implementation detail of Kotlin, it could change with time.

Value classes are more useful in some cases and less useful or entirely ineffective in others. Your case is actually a good example where they could be very useful. You have some kind of a model, you would like to protect against accidentally mixing e.g. user ID and comment ID, but also avoid additional heap allocations. Value classes allow your entity classes to store just String and all the code for processing the data, functions like e.g. getUserById(UserId) also use strings. Only because you need to add some automation magic that treats all ids similarly, value classes become ineffective and only during that process. I assume the reflection API itself incurs an order of magnitude bigger performance hit than wrapping/unwrapping something.

So yes, value classes are limited, there are many cases they need to be boxed. Still, in practice these cases aren’t that common. Why a very simple value-wrapper would be a part of a complex type system (supertype limitation)? It makes sense to implement interfaces like Comparable and there is always Any.toString. We can’t really expect a code based on comparators to magically know how to use entirely different types. Also, even if inlining manually, inside our head and using naked values everywhere, we still can’t pass an integer to a place where we need MyInterface.

I think collections is probably the biggest drawback. And this isn’t something new: we have a very similar problem with primitives in both Kotlin and Java. For this reason in performance-critical code we often use arrays or specialized data structures for storing primitives. In most cases though, we just don’t care. We don’t say List<Int> in Kotlin or List<Integer> in Java is unusable, because every write+read means boxing+unboxing.

1 Like

Thank you for this great summary. I mostly agree.

I get it that this feature is supposed to be automatic, and you shouldn’t care too much what happens under the hood, it “just works”.

My point is that the whole feature is about (mild) optimization, and you should take some informed decision whether it actually improves performance at all, or makes it worse. And yes, from the documentation I got the wrong impression that autoboxing takes place much less often than it actually does.

I agree that the usecase with which I started this thread is exceptional. Using IDs in collections, however, is not, that happens all the time. I wouldn’t say that using integers in collections is unusable. In fact, I really like how Kotlin treats primitive types, it is so much cleaner than in Java, but it comes with a price. In Java, if you put integers in an out of collections, without working with them numerically (which happens quite often), you may decide not to unbox them. You do not have this option in Kotlin.

You have this option with value classes, though. I mean, if you use them with collections a lot, you would just implement the ID as data class, not value class, wouldn’t you?

Anyway, as I mentioned, the problematic interoperability with Java code killed this feature for me anyway, so I wouldn’t use it at least for this usecase.

1 Like

For interpo with Java code, take a look at this KEEP

1 Like