Sealed class with object subclass: 'when' cannot smart cast object?


#1

See this snippet:

sealed class Foo {
  sealed class Bar(val name: String = "Bar") : Foo() {
    object Baz : Bar("Baz")
    class Qux(val quux: String = "Quux") : Bar("Qux")
  }
}

val baz: Foo = Foo.Bar.Baz
val qux: Foo = Foo.Bar.Qux()

val foos = listOf(baz, qux)

foos.forEach {
  val name = when (it) {
    Foo.Bar.Baz -> it.name
    is Foo.Bar.Qux -> "${it.name} and ${it.quux}"
  }
  println("Hello, $name!")
}

The when expression cannot smart cast it to Foo.Bar.Baz, so this does not compile because name is an unresolved reference. However, simply replacing the line with is Foo.Bar.Baz -> it.name allows for the smart cast to succeed and the code runs as expected. Why can’t the first situation, without is, do a smart cast? It has determined that it refers to the object, how could it be a different one and not have the name property? I understand that it is of type Foo here, but that’s where the smart cast could come in and help me.


#2

This seems to work fine for me. My output is:

Hello, Baz!
Hello, Qux and Quux!

Can you post the error message you see? Also which version of Kotlin are you using?


#3

I’m sorry! When I first edited my question I pasted the working code and forgot to remove is in the line that causes the error. I’ve edited my question again and it now does not compile any more (as intended originally). The question remains: why can’t an object match be implicitly smart cast to the object, instead of semi-explicitly using is and again a smart cast.

Also, I’m on Kotlin v1.2.60.


#4

You could just use Baz.name instead of it.name.

Smart casts are applied after type checks. The first line is not a type check. It is a value check. I know it seems like an arbitrary reason. But you will not get a better explanation.


#5

The reason why it is not cast smartly is because the when clause says nothing about the type of the Foo instance. It only says that that instance is equal to Foo.Bar.Baz, but being equal does not imply having the same type. I’ll give an extreme case as an example:

fun equals(other: Any) = true

As the compiler cannot conclude that being equal means having the same type, it cannot do a smart cast.


#6

Of course, thanks for pointing that out. I’ll try to remember this in the future that if working with an object, just use the object (and not it, in this case).


#7

That makes sense from the JVM point of view. However, the Kotlin compiler could, in theory, understand that it’s dealing with the object here. How could it refer to anything else at this point? It’s a val and its equals method returns true for the object. Doesn’t it violate the language specification if it then doesn’t behave like the equal object? It has to be of the same type. If it’s not, even if the equals method (or == operator) is overridden in some strange way to return true for our object (see example below), then still the compiler doesn’t allow to compare them due to different types.

For example (shortened for brevity, does not compile due to different types around ==):

object Baz {
  val name = "Baz"
}

class NotBaz {
	override fun equals(other: Any?): Boolean = other is Baz // Or simply always return true
}

val notBaz = NotBaz()
val name = if (notBaz == Baz) Baz.name else "" // Error: Operator '==' cannot be applied to 'NotBaz' and 'Baz'
println(name)

I know it’s all strange to use type checks, equality checks and objects like this, but I’m wondering if the compiler truly cannot conclude more than it does right now.


#8

Indeed the compiler might be able to conclude more in this situation. But should it? For the sake of consistency I would prefer the current state. Smart casts are currently only applicable after type checks. Adding them after value checks would introduce a whole new perspective with all new implications and inconsistencies that need to be thought through.