Auto-Delegation/Derivation of Interfaces with Inline Classes and Inline Extension Classes

Simply put: I want auto-derivation of inline classes that conform to interfaces (and I want the extension functions to be resolved as part of that too if possible).

Was bored this Friday afternoon and let my mind wander. This is a combination of multiple features. Feature 1, bystruct delegation:

interface ArrayLike<T> {
    val size: Int
    operator fun get(index: Int): T
}

fun <T> printHead(arr: ArrayLike<T>) {
    println("Element 1 of ${arr.size}: ${arr.firstOrNull()}")
}

inline class IntArrayLike(val arr: IntArray) : ArrayLike<Int> bystruct arr

fun main() {
    printHead(IntArrayLike(intArrayOf(1, 2, 3)))
}

Which means IntArrayLike essentially becomes this:

inline class IntArrayLike(val arr: IntArray) : ArrayLike<Int> {
    val size get() = arr.size
    operator fun get(index: Int) = arr.get(index)
}

This even has value outside of inline classes of course. Basically, it’s statically resolved structural typing. At compile time, the compiler confirms the RHS of bystruct conforms to the interface with static resolution. Props to those seeing the duck type benefits here. Here’s one important part that I’d really like: it includes resolution of extension functions where the class is defined. Not just explicit member function. Extensions being able to implement interfaces this way would be huge.

Feature 2, can we have inline object expressions that work like inline classes? So with some of the code above:

fun main() {
    printHead(intArrayOf(1, 2, 3).let {
        inline object : ArrayLike<Int> {
            val size get() = it.size
            operator fun get(index: Int) = it.get(index)
        }
    })
}

The benefit here goes without saying. Now, the anon/synthetic class count explosion here is as bad as with regular object expressions, but oh well. Granted, this is the equivalent of an inline class that accepts multiple constructor vals of the closure vars it captures, but it has value.

Feature 3, combining the previous 2 features, we have bystruct on object expressions, e.g.

fun main() {
    printHead(inline object : ArrayLike<Int> bystruct intArrayOf(1, 2, 3))
}

Whoa, cool. I just implemented an interface statically. But it’s a bit wordy.

Feature 4, instead of all of the words, we could;

fun main() {
    printHead(bystruct intArrayOf(1, 2, 3))
}

Now, this comes with the complication of resolving the generics of the interface which is annoying but can be done by looking at the iface methods or requiring the long form. But since that is complicated, an easier approach (that doesn’t require a new concept of unary keywords on expressions):

fun main() {
    printHead(intArrayOf(1, 2, 3).bystruct<ArrayLike<Int>>())
}

Of course, if the target has some kind of complex generic with compound interface requirements this should break. Another approach:

fun main() {
    printHead(intArrayOf(1, 2, 3) asinterface ArrayLike<Int>)
}

Other options include making it a callee/target concern. So for example, a bystruct parameter:

fun <T> printHead(bystruct arr: ArrayLike<T>) {
    println("Element 1 of ${arr.size}: ${arr.firstOrNull()}")
}

fun main() {
    printHead(intArrayOf(1, 2, 3))
}

This, again, has those same troubles of deriving generics but can be done. That’s mostly why I abandoned this option, and the fact that the target should not care how the iface is implemented, that’s the caller’s job. Another option is to make it the concern of the interface:

inline interface ArrayLike<T> {
    val size: Int
    operator fun get(index: Int): T
}

This could end up just creating an inline class of function references of sorts as a default impl, but that indirection causes performance issues sometimes (even with lazy MethodHandle bootstrap w/ invokedynamic). It also has the same generic derivation issue. I abandoned this idea for those reasons and similar reasons as above, it’s the caller’s choice how to implement the interface.

Another neat feature idea tangentially related but can help get to the same place (we’ll call it feature 5 though it’s not directly related to the others), inline inner extension classes:

inline inner class IntArray.IntArrayLike : ArrayLike<Int>

fun main() {
    printHead(intArrayOf(1, 2, 3).IntArrayLike())
}

Of course can’t have constructor params because what is extended is the single inline param. At this point, you don’t need to do delegation just like you wouldn’t need to if you extended IntArray and implemented ArrayLike<Int> at the same time. It can be assumed.

Just to confirm, everything I’m suggesting is statically resolved unlike Scala which used reflection to handle this and is regarded as a bad idea. Essentially, automatic delegation by static resolution of interface compatibility should be a thing IMO. Combined with inline classes, this should be doable without making everyone manually type out each function in these proxies. Take or leave any of the ideas, the keywords I made up, just spitballing here. I broke them up into separate feature mentions because by each one themselves has value. I am quite familiar w/ JVM bytecode and how some of these things might have to be implemented, but it’s mostly piggybacking on inline classes and their boxing approach.

Or, give Kotlin awesome macro-ability w/ stable API :slight_smile: We all want type providers and other things anyways, we promise not to slow down the compiler too much and we’re scared of the maintainability of compiler plugins.

2 Likes