Pairs and type inference

I am being puzzled by some aspects of type and nullability inference of Pairs.

Given these classes and enums

class Foo
class Bar
enum class Baz { BAZ }
enum class Qux { QUX }

val foos: List<Foo> = listOf(Foo())
val bars: List<Bar> = listOf(Bar())

These expressions do not compile — as one would expect:

Pair(1, Foo()) == Pair(Foo(), 1) // does not compile
Pair(Foo(), Bar()) == Pair(Bar(), Foo()) // does not compile
Pair(1, "string") == Pair("string", 1)  // does not compile
Pair(1L, 1) == Pair(1, 1L) // does not compile

But on the other hand, if I use lists or enums, the compiler accepts these:

Pair(foos, bars) == Pair(bars, foos) // compiles
Pair(1, foos) == Pair(foos, 2) // compiles
Pair(Baz.BAZ, Qux.QUX) == Pair(Qux.QUX, Baz.BAZ) // compiles

The nullability also confuses me:

val list: List<Pair<String, Int?>> = listOf("foo" to 1, "bar" to null)
val filtered: List<Pair<String, Int>> = list.filterIsInstance<Pair<String, Int>>()
assert(filtered.size == 2) // why‽

Why does filterIsInstance not filter the second item out?
Can someone explain what’s going on?

I always felt like this restriction on types used with equality operator is kind of hacky. Java/JVM allows us to make objects of different types to be equal. Kotlin does not complain if we try to compare supertype with its subtype. But wait, what about this code:

open class Base { ... }
class Subtype1 : Base() { ... }
class Subtype2 : Base() { ... }

val base = Base()
val subtype1 = Subtype1()
val subtype2 = Subtype2()

println(base == subtype1) // true
println(base == subtype2) // true
println(subtype1 == subtype2) // compile error

If base equals subtype1 and base also equals subtype2 then wouldn’t it make sense to assume subtype1 should equal subtype2? But such check isn’t even allowed, because it is assumed to never be true.

Also, == works differently regarding type inference than anything else in the language. Normally, the compiler tries to automatically cast objects to their supertypes to make the code compile. Note that we can safely cast Pair<Foo, Bar> to Pair<Any, Any> and now equality operator works just fine, Usually, compiler does this for us. See this example:

fun <T> checkEquality(obj1: T, obj2: T)

This function can be invoked as checkEquality(Foo(), Bar()), because then both objects are automatically cast to Any. Equality operator does not do this. It seems to have its own logic for comparing provided types. Maybe it was hardcoded to verify first level of parameter types, but it ignores deeper types?

And regarding filterIsInstance() - I consider this function not really type safe. It checks raw type only and at all ignores parameter types. It actually can’t do better for technical reasons, but this is why I think it should be disallowed to use it with parameterized types. I described this problem here:

1 Like

Thanks for your input!
That settles the case for filterIsInstance

I’m rather ok with equality being typesafe, and I think this is an improvement over Java. I have no problem with objects subtype1 and subtype2 deemed not equal by the compiler within a context where one is cast as Subtype1 and the other as Subtype2 because I probably shouldn’t test equality in this context anyway.

(fun(b1: Base, b2: Base): Boolean { return b1 == b2 }(subtype1, subtype2))

this compiles and this is fine. Anyway I understand this is a design choice and it’s just my humble opinion.

Still, I’m confused with == behaviour for Pair:

Pair(subtype1, subtype2) == Pair(subtype2, subtype1) // compile error

gives compile error so there’s no upcasting. Which makes the enum example weird because the only explanation I came up with is that they be upcast as Enum<T>.

Pair(foos, bars) == Pair(bars, foos) // compiles

suggests that there is something about type erasure, but then

Pair(1, foos) == Pair(foos, 2) // compiles

kills me. How is that even possible?