Smart cast and type inference of generics


#1

See the code below:

interface A

class B : A

fun <T> identity(a: T): T = a

fun <T> f1(b: T) {
  if (b is B) {
    val tmp = identity(b)
    val c: B = tmp // Error:(12, 16) Kotlin: Type mismatch: inferred type is Any? but B was expected
  }
}

fun <T> f2(b: T) {
  if (b is B) {
    val tmp: B = identity(b)
    val c: B = tmp // this compiles ok.
  }
}

In both cases, the type of variable tmp is determined in compile time, and can be inferred directly.

while function f1 doesn’t compile and throw “Error:(12, 16) Kotlin: Type mismatch: inferred type is Any? but B was expected”, function f2 compiles without errors.


#2

A quick quiz

if ( b is B1 && b is B2 ) {
    val tmp = identity(b)
}

what is the type of tmp?


#3

It should be the intersection of B1 and B2, but Kotlin doesn’t have it.


#4

It should be the union of both


#5

I don’t understand. Will the tmp of the below be union of B1 and B2?

if( b is B1 || b is B2){
    val tmp = identity(b)
}

#6

It should be the union, but it is the intersection, which is Any since there is no other type common to B1 and B2 (since Kotlin doesn’t have union types).


#7

Why are you changing the Boolean operator from && to || in the middle of the conversation? It makes it very confusing to discuss.

In my opinion the answer to the original quiz (b is B1 && b is B2) should be the intersection. This expression can only be true if B1=B2 or there is a subtype relation between them. In the later case the answer would be the subtype, which effectively is the intersection, isn’t it?

Wouldn’t it be B1 or B2, whichever is the subtype of the other?


#8

B1 and B2 in the example can be interfaces, ie: B1 = Comparable and B2 = CharSequence, so a String is an instance of both, so I can invoke both compareTo and subSequence.

Kotlin sarch for the greatest common parent, in the @wumo example B1 is B and B2 is the T upper bound Any?, so the intersection between B and Any? is Any?.


#9

I think it is a shortcoming of the compiler. The compiler knows that b is a B (if you assign b instead of tmp to c, the compiler does not emit errors). So the compiler should also be able to correctly infer that the result type of identity(b) is B (because it is the most specific type that the compiler knows about).

If you start combining multiple is expressions, then the compiler has no choice but to choose the least specific type that satisfies the combined expression.


#10

The SmartCaster is just being a bit greedy with its types.

Line by line:

  1. SmartCaster deremines that b is B
  2. You assign tmp to identity(b). At this point it asks “what is <T>?” and determines b : T, so T = T. I suppose SmartCast logic COULD determine that b is B means T is a subclass of B and use identity<B>(b), but at this point that comparison only exists in this floating SmartCast logic, not as an actual trait of b, so it just grabs the type it knows.
  3. Now that tmp : T, the smartcast has no idea that tmp is B. It doesn’t know that identity returned b, it just knows it got a T.

In this case there would probably be no harm in smart casting identity(b) to identity<B>(b) rather than identity<T>(b), but in general I hesitate trying to make the SmartCaster too clever. It’s not meant to meta-compile and logically deduce everything that’s logically deducible; it’s just meant to grease the wheels and make nullables and type casting not require a ton of redundant checks in even the simplest cases.


#11

Have only glanced at the new contract stuff that is coming, but would it let you tell the compiler enough to get around this?


#12

Why do you expect T to be inferred as B in f1