Kotlin inline "value" Generics

Right now, inline “value” classes provide an amazing way to reduce memory usage and improve cache efficiency, but are largely incompatible with generics, which force allocations, and so they are therefore difficult to efficiently use with Containers, Iterators, Streams, and such. If Kotlin allowed classes to have inline “value” generics (presumably requiring primitive lower bounds), then this could be used to make pseudo-containers, pseudo-iterators, and pseudo-streams for value classes, making them dramatically easier to use, and reduce memory usage.

class LongValueList<value T: Long>(
    private val data = androidx.collection.LongList()
) { 
    inline fun any(pred: (T)->Boolean) = data.any(pred)
    operator fun contains(T) = data.contains(e)
    fun containsAll(es: ValueList<out T>) = data.containsAll(es)
    fun count(pred: (e: T)->Boolean) = data.count(pred)
    fun elementAt(idx: Int) = data.elementAt(idx)
    fun elementOrElse(idx: Int, defaultVal:(Int)->T) = data.elementOrElse(idx, defaultVal)
    // etc
} 

During compilation, the type-erasure would merely “inline” the T to be Long.
Additionally, kotlin stdlib would also add pseudo-containers mirroring Androidx’s LongList, LongLongMap, LongLongPair, LongObjectMap, LongSet, LongSparseArray, and ObjectLongMap, with built-in support for value generics. Also, ideally kotlin stdlib would add LongQueue and LongDequeue, which are not currently provided by Androidx.

Most of the value class work is blocked on seeing what Project Valhalla does. It should, I believe, offer some of the optimizations you want!

1 Like

Please correct me if I’m wrong, but I think what you’re asking for is not specific to value classes, but rather the JVM in general? Cause you would still have the boxing and unboxing for primitives when using them with generics, even if you’re not using Kotlin value classes.

1 Like

I don’t really understand your case. Long is final, so is there any use of T: Long? Isn’t your example effectively the same as non-generic class where you replace T with Long everywhere? And such class includes optimizations you ask for already.

kyay10: Project Valhalla looks amazing at a glance, but it’s not immediately clear if it actually solves this problem. I’ll have to spend more time digging into the details.

Skater901 & Broot: My suggestion was for Kotlin to allow <value T: Long> as a pseudo-generic. Broot is mostly correct that all I’m asking for is syntactic sugar around a non-generic class in the JVM, where all of the T were replaced with the primtive long, and would therefore not require boxing at any point. I can emulate this with an annotation processor today, but the annotation-processor generated classes would all be entirely identical, other than path and name, which is a waste of compile time and binary size.

State of Valhalla: Part 1: The Road to Valhalla ” does clearly state

The plan for generics has two phases: universal generics and specialized generics. In the first phase, we heal the rift at the language level that prevents us from using primitive types as generic type parameters, allowing generics to range over all types — but still implemented via erasure…
In the second phase, we enable layout and code specialization in the JVM for generic classes and methods …

So yes, that project does appear to be tackling the same goal, and I’ll just use annotations for now.

I don’t think this is currently possible, on the JVM. I guess if you’re only using it for your own code, which can then have the generic type replaced with the primitive at Kotlin compile time, then that’d be fine. But if you ever had List<value T : Long>, that would get replaced with a Long and not a long, IE the boxed type and not the primitive type. This is not something that Kotlin can solve because it’s a limitation of the JVM.

I think we’re on the same page about all this, and that Project Valhalla will fix this, but my point is that asking for this on the Kotlin forums kind of doesn’t make sense, because you’re running up against a JVM limitation, not a Kotlin limitation.

I’m confused about something fundamental here. It seems that your generic is actually always fixed to one value, no? Why not just do this instead then:

class LongValueList(
    private val data = androidx.collection.LongList()
) { 
    inline fun any(pred: (Long)->Boolean) = data.any(pred)
    operator fun contains(Long) = data.contains(e)
    fun containsAll(es: ValueList<out Long>) = data.containsAll(es)
    fun count(pred: (e: Long)->Boolean) = data.count(pred)
    fun elementAt(idx: Int) = data.elementAt(idx)
    fun elementOrElse(idx: Int, defaultVal:(Int)->Long) = data.elementOrElse(idx, defaultVal)
    // etc
}

I’m definitely misunderstanding your intent here.

That’s what I meant above. This class is effectively non-generic, so I don’t understand why we even use generics in the first place.

1 Like

The reason for using “generics” is for the type safety from value classes. Consider the following example:

value class PhoneNumber(private val number: Long) {
    override fun toString(): String = number.toString().padStart(10, "0")
}

value class SSN(private val socialSecurityNumber: Long)

val longValueList = LongValueList(PhoneNumber(123456))
longValueList.containsAll(LongValueList(SSN(1234))) // <---- compile time error, SSN and PhoneNumber are not the same type

The goal is to have the type safety of value classes (even though they both use a Long, they’re different types) at compile time, but at run time, those value types would be erased, and everything would just be longs.

Why would this even compile? PhoneNumber and SSN are not longs or subtypes of it. They are classes entirely separate from Long.

I see what you try to achieve here and I like the feature, but you try to hack through language features that mean an entirely different thing. Value classes conceptually use composition, not inheritance, so using them like this feels wrong to me.

2 Likes

@Skater901 It is possible, as the Kotlin compiler already replaces value class references with the underlying type, in the case the primitive long.

You’re right that it wouldn’t work for collections like List<T> as those use real genetics and type erase to Object and thus require boxing. However, what I asked for was PSEUDO genetics, with the syntax <value T: Long>. Just like the Kotlin compiler replaces value class references with primitives, it would do the same for the proposed pseudo-generic, creating a non-generic class using primitive long under the covers. I suggest this specifically to sidestep the limitations of the underlying JVM. We aren’t blocked by Valhalla.

@broot You’re right that conceptually, value classes use composition rather than inheritance. I had also contemplated proposing instead that value classes have (autogenerated?) operators for converting to and from the underlying types, and then a class could have some sort of PSEUDO generic declaration that constrains to classes that can be “cast” to and from the specified primitive, but in the end that seemed to be a lot of complexity for what is fundamentally the same thing.
Value types implementing an interface and then the class constraining to that interface almost suffices… but in the end we still want a pseudo-generic that erases to a primitive instead of a real generic that erases to Object, so using an interface doesn’t really help much, unless the interfaces themselves are baked into the language, which doesn’t seem like a good direction to go. Pretending value classes inherit instead of compose was trivial, and doesn’t actually cause any issues.

You can get pretty close to this idea, with the hope that the compiler won’t box your value classes if everything is inline (that’s not guaranteed, of course, but IIRC the compiler does optimize this when it can):

fun usage(ll: LongValueList<SSN>) = with(SSN) {
    ll.any { it.isValid }
    ll.contains(SSN(42))
    val ssn: SSN = ll.elementAt(2)
}

interface LongLike<T> {
    fun Long.toValue(): T
    fun T.toLong(): Long
}
// Bridge methods
context(converter: LongLike<T>) 
inline fun <T> Long.toValue(): T = with(converter) { toValue() }
context(converter: LongLike<T>) 
inline fun <T> T.toLong(): Long = with(converter) { toLong() }

class LongValueList<T>(@PublishedApi internal val data: LongList) {
    context(_: LongLike<T>)
    inline fun any(pred: (T) -> Boolean) = data.any { pred(it.toValue()) }
    context(_: LongLike<T>)
    inline operator fun contains(t: T) = data.contains(t.toLong())
    context(_: LongLike<T>)
    inline fun containsAll(es: LongValueList<out T>) = data.containsAll(es.data)
    context(_: LongLike<T>)
    inline fun count(pred: (e: T) ->Boolean) = data.count { pred(it.toValue()) }
    context(_: LongLike<T>)
    inline fun elementAt(idx: Int) = data.elementAt(idx).toValue()
    // etc
}

@JvmInline
value class SSN(private val socialSecurityNumber: Long) {
    val isValid: Boolean get() = TODO()
    companion object: LongLike<SSN> {
        override fun Long.toValue() = SSN(this)
        override fun SSN.toLong() = socialSecurityNumber
    }
}

I haven’t seen context before, so that’s something I’ll have to investigate, to learn how the LongValueList is finding the companion object.

I had considered a converter design that was similar, but my concern with that sort of design though, is that elementAt is Object(Int) in the JVM, and contains is Boolean(Object) in the JVM, and those Objects will force boxing and unboxing, causing heap usage and cache misses. At that point, you’d get better performance by just…. not using value classes. And I’m not even remotely convinced that the JVM will fork the LongValueList class to specialize the methods for primitives. In fact, the “Parametric JVM” part of Project Valhalla seems to be exactly that. Changing the JVM so that it can specialize generic classes (not just for primitives).

I had not considered simply marking all of the methods as inline. That probably would solve the problem. My IDE at least shows warnings about that, but I could suppress them.

Yeah I think inlining might do the trick, at least as long as the compiler is clever enough. Even still, I’m sure a very simple optimizer can be written for this specific case (and maybe contributed back to R8 or proguard or something).
Basically, by inlining, it’s known for a fact that 42L.toValue() returns not just any object, but an SSN, so the hope is that the compiler then calls the concrete (and mangled) method Long.toValue_but_mangled(): Long on SSN.Companion instead of calling the Long.toValue(): Object interface method, and similarly for toLong. Thus, the boxing calls are avoided

They’re not classes entirely separate from Long, at least not as far as I understand Value Classes. Value Classes are a purely compile-time constraint, and at run time, every usage of a Value Class gets replaced with its actual type. Technically it’s not inheritance, yes, but the concept makes sense to me; anything you can do to a Long, you should be able to do to a PhoneNumber or SSN. However, while you can have a collection of Longs, if you have a collection of PhoneNumbers, it shouldn’t contain any SSNs.

This is wrong, since you’re still using generics for lambdas as far as I understand. Even if you had fun count(pred: (e: Long) -> Boolean), the (e: Long) -> Boolean under the hood gets mapped to a Function1<Long, Boolean>, so the types still get boxed, I think.

If you inline the lambda it’s fine!
You can also make your own fun interface LongPredicate { operator fun invoke(e: Long): Boolean }

1 Like