Why are these types incompatible? Potential workarounds?

Hello, I am (attempting) to write a library that supports both mutable and immutable representations of genetic data. Obviously, the mutable representation of the data should adhere to the same interface as the immutable representation because it can perform all the same functionality as the immutable representation. This is where I start to run into some trouble.

The data is represented in a tree structure. Any node that has children is an “Ancestor” while nodes that carry genetic information are called a “Feature.” A node that is an ancestor but not a feature is a “Genome,” which represents the root of the entire tree.

Here is a reduced version of the code to show subtyping relationships.

public sealed interface Ancestor : Iterable<Feature>
public sealed interface MutableAncestor : Ancestor, Iterable<MutableFeature>
public sealed interface Feature
public sealed interface MutableFeature : Feature

My assumption was that since the iterator only ever produces and never consumes the type specified, that this type of override would be legal. However, the compiler produce the following error “Type parameter T of iterable has inconsistent values: Feature, MutableFeature.” It is not clear to me why this arrangement would cause an issue. I want to ensure that the client can get access to a node as a MutableAncestor and concisely iterate over its descendents without having to cast each one of them to a MutableFeature.

Is there a good workaround to accomplish what I want to accomplish?

Looks like KT-13380. You can simply do:

public sealed interface Ancestor {
    public operator fun iterator(): Iterator<Feature>
}
public sealed interface MutableAncestor: Ancestor {
    public override fun iterator(): Iterator<MutableFeature>
}
public sealed interface Feature
public sealed interface MutableFeature : Feature

or:

public sealed interface AncestorBase<out F: Feature> : Iterable<F>
typealias Ancestor = AncestorBase<Feature>
public sealed interface MutableAncestor : AncestorBase<MutableFeature>
public sealed interface Feature
public sealed interface MutableFeature : Feature

// A `MutableAncestor` can be passed where an `Ancestor` is needed

fun needsAncestor(a: Ancestor): Unit = TODO()
fun hasMutableAncestor(a: MutableAncestor): Unit = needsAncestor(a)
1 Like

Thank you! I understand the second solution, but I have a question about the first. Would the first one permit the client to write

val ancestor : MutableAncestor = //some sort of initialization
for (descendant in ancestor) {
//body of loop
}

even though it’s not explicitly declared as Iterable? I want to ensure clients can use the simplest syntax possible.

1 Like

Yes. That’s because the iterator function is an operator, and so it is the one used for for in loops

1 Like

Exactly what I needed thank you so much!

1 Like