Inference with unexpected outcome


#1

Hi,

I’ve written the following example:

(Running with JavaScript gives a better output because it concats the array content)

The first function call to containsAll outputs the result that I would expect, that “Hello” and “World” are both within the Iterable.

However, the second function call should, in my opinion, not be allowed by the compiler.

The type of T should in the second call also be infered to String and therefore arrayOf without spreading should not be allowed.

If I compile it on the JVM with the following changes:

inline fun <reified T, reified I : Iterable<T>> I.containsAll(vararg items: T) {
    println("I is Iterable<"+ I::class.typeParameters.joinToString() +">")
    println("T is " + T::class.simpleName)
    items.forEach {
        println("Iterable contains $it: ${this.contains(it)}")
    }
}

fun main(args: Array<String>) {
    val iterable: Iterable<String> = listOf("Hello", "World")

    iterable.containsAll("Hello", "World")
    println()
    iterable.containsAll(arrayOf("Hello", "World"))
}

it prints

I is Iterable<out T>
T is String
Iterable contains Hello: true
Iterable contains World: true

I is Iterable<out T>
T is Serializable
Iterable contains [Ljava.lang.String;@11e21d0e: false

Process finished with exit code 0

Is this expected behavior?

If Array does implement Serializable, it kind of makes sense but still feels strange.

Changing the example to:

data class MyString(val str: String)

inline fun <reified T, reified I : Iterable<T>> I.containsAll(vararg items: T) {
    println("I is Iterable<"+ I::class.typeParameters.joinToString() +">")
    println("T is " + T::class.simpleName)
    items.forEach {
        println("Iterable contains $it: ${this.contains(it)}")
    }
}

fun main(args: Array<String>) {
    val iterable: Iterable<MyString> = listOf(MyString("Hello"), MyString("World"))

    iterable.containsAll(MyString("Hello"), MyString("World"))
    println()
    iterable.containsAll(arrayOf(MyString("Hello"), MyString("World")))
}

it just infers Any:

I is Iterable<out T>
T is MyString
Iterable contains MyString(str=Hello): true
Iterable contains MyString(str=World): true

I is Iterable<out T>
T is Any
Iterable contains [MyString;@11e21d0e: false

Is this expected behavior? It feels kind of dangerous to me :-/


#2

I guess there is a bug in the generic system regarding varargs and reified types. This definitely should not happen. I tested it also and you can pass anything to your function. It just says the type of T is Any.


#3

That’s a correct inference result in this case, it satisfies the constraints specified in the function signature.


#4

So this is expected behavior by design?

fun <T> Iterable<T>.fun2(item: T) = Unit

fun <T> fun3(iter: Iterable<T>, value: T) = Unit

fun main(args: Array<String>) {
    val iterable: Iterable<String> = listOf("Hello", "World")
    iterable.fun2(5)
    fun3(iterable, 5)
}

This compiles fine and infers everything happily to Any.

While using this in Java:

public static void main(String[] args){
  List<String> it = new ArrayList<>();
  it.add("A");
  fun(it, 5);
}

private static <T> void fun(Iterable<T> it, T value) {
}

Results in a compile time error:

error: method fun in class JavaFiddle cannot be applied to given types;
      fun(it, 5);
      ^
  required: Iterable<T>,T
  found: List<String>,int
  reason: inference variable T has incompatible bounds
    equality constraints: String
    lower bounds: Integer
  where T is a type-variable:
    T extends Object declared in method <T>fun(Iterable<T>,T)

The same applies to C#:

public class Program
{
	public static void Main()
	{
		var list = new List<String> { "A", "B" };
		list.ShouldContainAll(1);
	}
}

public static class Extensions
{
	public static void ShouldContainAll<T>(this IEnumerable<T> ie, T t) {
		
	}
}
Compilation error (line 9, col 3): The type arguments for method 'Extensions.ShouldContainAll<T>(System.Collections.Generic.IEnumerable<T>, T)' cannot be inferred from the usage. Try specifying the type arguments explicitly.

Having this class in Kotlin:

class MyBox<T>(val value: T) {
    fun contains(value: T) = Unit
}

calling MyBox("String").contains(3) is a compile time error:

The integer literal does not conform to the expected type String


#5

I get that it does, kind of, but how would you specify the constraints to achieve what @magges intended? The thing I don’t get is why this should work that way. Kotlin is normally against implicit type conversions so why is it ok to implicitly cast the type of the iterable from Iterable<Int> to Iterable<Any> or even to Iterable<Serializable> (see first example).
IMO this only leads to difficult to find bugs and I would in this case prefer it to work the way Java and C# does.


#6

In Java Iterable interface is invariant, and in Kotlin it’s covariant. Usually Java methods taking Iterable are declared as:

private static <T> void fun(Iterable<? extends T> it, T value) { }

And that allows calling it the same way as in Kotlin: fun(it, 5); where it is a List<String>.

Casting Iterable<Int> to Iterable<Any> is an upcast in Kotlin, same as casting Int to Any. Do you feel that the latter should be prohibited?

As to why Any is inferred for T there — it’s the same reason why we infer a common supertype of two types in listOf(A, B) where A and B are some subtypes of that common supertype.

There are some internal annotations that alter how type inference and overload resolution work, e.g. NoInfer, OnlyInputTypes, Exact, see https://github.com/JetBrains/kotlin/blob/1.2.50/libraries/stdlib/src/kotlin/internal/Annotations.kt.
However we haven’t made them available public yet, because we’re unsure of the design of these features.

We admit that the use cases for these annotations exist, we know that because the introduction of these annotations was driven by the needs of the standard library functions, for example Iterable.contains.


How prevent type inference?
#7

No, I don’t. But this example and the once in the standard library show that this is something that is needed. I wasn’t aware of the internal annotations. Obviously we can’t use them ourself but it is good to know that this is at least a known problem. Is there a KEEP or an issue about this?


#8

The problem is that all use of the type variable in the signature are used to determine a valid type, even when that is not intended (as such the annotations). For now the way to do it is to specify the type explicitly.

Btw. Java has the same issue. Sometimes using an extra variable that is a subtype of the first can be used to “fix” type resolution.


#9

@zcb7kbgxtgjt found a workaround to use those internal annotations. Not sure if this workaround is a bug in kotlin itself.