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…
https://try.kotlinlang.org/#/UserProjects/bokbcrjveih1cg476rdjcv7edm/mvic5eh1qcahn9vd18ctgf7e8i
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.)