Bug, language confusion, or what? (Passing intersection-typed arguments)


#1

I’m sure I’ve worked through many situations in Kotlin where Smart Cast indicated that it recognized that an instance has been identified as more than one class, presented its type as an intersection of those types, and fairly effortlessly allowed me to refer to unique attributes of either class via the same instance reference.

I’ve also written extension functions that work on receivers specified purely by an intersection of types.

… So, a scenario I hit today is surprising me. Is this a regression…? Possibly the objects I’ve provided to functions accepting generic intersection types always happened to have a declared type that manifests the intersection known at compile time, and that this is just some bizzare hole in the language I’ve managed not to notice because the IDE acted like it supported it?

What am I missing?
See the following…

package intersection_type_bug.in_kotlin

interface A
interface B

object SomeOtherKotlinFile
{
    private class Ab : A, B

    fun getAnAThatWeKnowIsAlsoAB(): A = Ab()
    fun getANullableAThatWeKnowIsAlsoAB(): A? = Ab()
}

fun <TAb>
acceptAnAThatIsAlsoAB( anAThatIsAlsoAB: TAb )
    where TAb : A,
          TAb : B
    = Unit

fun
main( args: Array<String> )
{
    fun
    ignoreExceptions( testBlock: () -> Unit )
    {
        try { testBlock() }
        catch( t: Throwable ) { println( t ) }
    }

    run {
        val startedAsNullableA: A? = SomeOtherKotlinFile.getANullableAThatWeKnowIsAlsoAB()
        startedAsNullableA as B
        startedAsNullableA // OK - 'Expression Type' action says type is "B & A (smart cast from A?)"
        acceptAnAThatIsAlsoAB( startedAsNullableA ) // <- This is the goal.
// ERROR-^
// Type parameter bound for TAb in
// fun <TAb : A> acceptAnAThatIsAlsoAB(anAThatIsAlsoAB: TAb): Unit where TAb : B
// is not satisfied: inferred type Any is not a subtype of A
    }

    run {
        val startedAsNullableA: B? = SomeOtherKotlinFile.getANullableAThatWeKnowIsAlsoAB() as B?
        startedAsNullableA as A
        startedAsNullableA // OK - 'Expression Type' action says type is "A & B (smart cast from B?)"
        acceptAnAThatIsAlsoAB( startedAsNullableA )
// ERROR-^
// Type parameter bound for TAb in
// fun <TAb : A> acceptAnAThatIsAlsoAB(anAThatIsAlsoAB: TAb): Unit where TAb : B
// is not satisfied: inferred type Any is not a subtype of A
    }

    run {
        val startedAsA: A = SomeOtherKotlinFile.getAnAThatWeKnowIsAlsoAB()
        startedAsA as B as A
        startedAsA // 'Expression Type' action says type is "B (smart cast from A)"
        acceptAnAThatIsAlsoAB( startedAsA )
// ERROR-^
// Type parameter bound for TAb in
// fun <TAb : A> acceptAnAThatIsAlsoAB(anAThatIsAlsoAB: TAb): Unit where TAb : B
// is not satisfied: inferred type Any is not a subtype of A
    }
    run {
        val startedAsA: A = SomeOtherKotlinFile.getAnAThatWeKnowIsAlsoAB()
        startedAsA as B
        startedAsA as A
        startedAsA // 'Expression Type' action says type is "B (smart cast from A)"
        acceptAnAThatIsAlsoAB( startedAsA )
// ERROR
// Type parameter bound for TAb in
// fun <TAb : A> acceptAnAThatIsAlsoAB(anAThatIsAlsoAB: TAb): Unit where TAb : B
// is not satisfied: inferred type Any is not a subtype of A
    }
    run {
        val startedAsA: A = SomeOtherKotlinFile.getAnAThatWeKnowIsAlsoAB()
        startedAsA as (B & A)
        // ERROR -------^
        // Expecting comma or ')'
    }
    run {
        val startedAsA: A = SomeOtherKotlinFile.getAnAThatWeKnowIsAlsoAB()
        startedAsA as B & A
        // ERROR -------^
        // Unexpected tokens (use ';' to separate expressions on the same line)
    }
    run {
        val startedAsA: A = SomeOtherKotlinFile.getAnAThatWeKnowIsAlsoAB()
        startedAsA as (B | A)
        // ERROR -------^
        // Expecting comma or ')'
    }
    run {
        val startedAsA: A = SomeOtherKotlinFile.getAnAThatWeKnowIsAlsoAB()
        startedAsA as B | A
        // ERROR -------^
        // Unexpected tokens (use ';' to separate expressions on the same line)
    }
    run {
        val startedAsA: A = SomeOtherKotlinFile.getAnAThatWeKnowIsAlsoAB()
        startedAsA as object : B, A
        // ERROR -----^
        // Type expected
    }
    run {
        var voldemort1 = object : B, A {}
        acceptAnAThatIsAlsoAB( voldemort1 ) // OK, but not what we want

        var voldemort2 = if( true ) null else object : B, A {}
        ignoreExceptions {
            acceptAnAThatIsAlsoAB( voldemort2!! ) // OK (at compile time), but not what we want
        }

        val startedAsA: A = SomeOtherKotlinFile.getAnAThatWeKnowIsAlsoAB()
//        voldemort2 = startedAsA // No known way to cast
//        // ERROR
//        // Type mismatch: inferred type is A but <no name provided>? was expected

        voldemort2 = startedAsA.asTypeOf { voldemort2 } // OK at compile time
        // ERROR at run time ---^
        // java.lang.ClassCastException: intersection_type_bug.in_kotlin.Ab cannot be cast to intersection_type_bug.in_kotlin.Sample1Kt$main$2$voldemort2$1
        acceptAnAThatIsAlsoAB( voldemort2!! ) // OK at compile time

        val voldemort3 = startedAsA.asTypeOf { object : B, A {} } // OK..
        acceptAnAThatIsAlsoAB( voldemort3 ) // OK!

        acceptAnAThatIsAlsoAB( startedAsA.asExpectedType() )
        // ERROR -^                       ^
        // ERROR -------------------------|
        // Type inference failed: Not enough information to infer parameter [...]
    }
}

//inline fun <T1 : Any, T2 : Any>
//Any.asIntersectionType()
//{
//    object : T1, T2 {}
//    // ERROR-^
//    // Only classes and interfaces may serve as supertypes
//}
//
//inline fun <reified T1 : java.lang.Object, reified T2 : java.lang.Object>
//Any.asIntersectionType()
//{
//    object : T1, T2 {}
//    // ERROR-^
//    // Only classes and interfaces may serve as supertypes
//}

inline fun <TDesired>
Any.asTypeOf( producerOfDesiredType: () -> TDesired ): TDesired
    = this as TDesired

inline fun <TExpected>
Any.asExpectedType(): TExpected
    = this as TExpected

The basic equivalent in Java is below. I’m thinking the above must be a regression from an earlier Kotlin version?

package intersection_type_bug.in_java;

public class Sample1
{
    static interface A {}
    static interface B {}

    static class Ab implements A, B
    {
    }

    static A getAbAsA() { return new Ab(); }

    static <TAb extends A & B> void acceptAnAThatIsAlsoAB( TAb anAThatIsAlsoAB )
    {
    }

    public static void main( String[] args )
    {
        {
            A startedAsA = getAbAsA();
            acceptAnAThatIsAlsoAB( (A & B)startedAsA ); // OK
            System.out.println( "Done" );
        }
    }
}

(Is it just me that thinks the term intersection type sounds like a misnomer? I suppose it’s just a matter of the perspective from which you’re thinking about it.)


#2

I don’t know if there is a direct solution but as a workaround you can just use a third interface.

interface A
interface B
interface AandB : A, B

#3

I wish it were that simple. :\

For me to have even considered the tedious choice of addressing the interfaces either separately or via verbose intersections means the alternative must have seemed worse, and possibly impossible at the time.

(When trying to juggle hierarchies including Java 8 interfaces for the availability of default methods, Kotlin interfaces for all the other language perks, and full classes that include methods you want to override from a framework, you hit a lot of interop issues (which seem to be getting better over time) and it gets tricky, particularly because of the often undesirable and sometimes confusing heuristics used to choose which method implementations get resolved to in the end. I miss how straightforward multiple inheritance was in C++.)

I believe it might have been technically possible to add such a convenience interface for just this particular usage, but if I recall correctly, my appraisal of that option – at the very least – included improperly implying that that merger interface existed in all cases in which the interfaces intermixed, which was fairly dangerous IMO.

For now, I’m just creating another Java helper class, until a more official answer comes around.
(What’s the annotation to mark a Java method as an extension function?)


#4

Does this solve your problem?

interface A
interface B
class Both : A, B

fun <T> myFunction(aAndB: T) where T : A, T : B {
	println(aAndB)
}

#5

It looks like a bug to me.

if (value is A and value is B) {
    // value is A and value is B here, but not A & B
}

There seems to be no way to cast a value to an intersection type.