Smart cast for two subclasses of a sealed class?


#1

I think it would be useful if the compiler could work on sealed case classes with only two implementing classes in a similar way to checking for null and smart casting after the check:

sealed class Something {
    class SomeA : Something()
    class SomeB : Something() {
        fun smartCast(): String = "Smart cast"
    }
}

fun test(sth: Something): String {
    if (sth is SomeA) return "A"
    return sth.smartCast()
}

Of course this doesn’t work at the moment, but when the function did not return after checking if sth is SomeA then sth is of type SomeB, right?


#2

I don’t know how deep you want to go into “x is deducible from the current situation, therefor you don’t have to explicitly state x” style smart-casting. It doesn’t exactly make for the most readable code.
Reading if( x is SomeB) return x.smartCast() is pretty clear exactly what it does and why (especially since, ideally, the method smartCast should have something to do with WHAT SomeB is. But reading if( x !is SomeA) return x.doWhatSomeBDoes(), the code is pretty obtuse even if you’re fairly familiar with the fact that there are only two types of Somethings.

However, what Kotlin CAN do with sealed classes is view them as a “complete” (i.e. covering all cases) when statement allowing you to use it as an determined value:

sealed class Something
class SomeA : Something()
class SomeB : Something() {
    fun smartCast(): String = "Smart cast"
}

fun test( sth: Something) = when( sth) {
    is SomeA -> "A"
    is SomeB -> sth.smartCast()
}

#3

Yes, but if say SomeA is actually called Invalid and SomeB is Valid then reading something like

if (result is Invalid) return result.errors.first()
return result.value

makes sense to me, because when the method did not return, the result is not Invalid and therefore must be Valid making the happy path the path least nested and/or obscured by syntax.

But yeah, I see that this can be easily misused.


#4

I can see that. I personally prefer to read and write comprehensive whens, but null smart-casting already work with implicit scopes, so it’s not unreasonable to expect enums/sealed classes to behave the same way.


#5

Well with Nullability it is guaranteed to be one of 2 cases. I agree the compiler could check whether or not the enum/sealed class has only 2 implementations, but that would mean you can make a lot of code invalid by just adding a third value to an enum, which would be really bad for library development as you can no longer extend your enums/sealed classes without breaking backwards compatibility.


#6

To be fair, is there any way to expand a sealed class without breaking things that use the fact that it’s sealed? Adding a new Sealed type will break a when statement (though perhaps in a more intuitive way that is easier to fix) and an if…else sequence without a fallback return. And if you aren’t using those then what benefit are you get out of declaring it sealed rather than just final?

The keyword is called “sealed” for a reason. While adding a new Sealed type is certainly a realistic situation, if code is expecting there to only be 3 different Sealed types, isn’t the whole point that it breaks at compile time when you add a 4th so that you can handle the new type?