I wish `when` had better smart casts. I'm often hi...
# announcements
p
I wish
when
had better smart casts. I'm often hitting this case:
Copy code
open class Animal
class Cat : Animal()
class Dog : Animal()

when (any) {
  is Cat, is Dog -> // I wish `any` had type `Animal` here, not `Any`
}
l
That would make a lot of sense to me
👍🏻 1
p
it can be solved by splitting the two cases, but that results in code duplication (or requires to extract a method).
a
If you want to operate on the animal open class, why not is on animal? 🤔
c
yeah the question here is more: why doesn’t default equal the animal case?
a
Would be hard to reckon what you actually want i guess. What if animal implements an interface bird f.ex?
p
@Anders Mikkelsen I only want to operate on
Animal
in the
is Cat, is Dog
branch of the
when
expression. other branches aren't shown here.
@codeslubber this is a simplified example. imagine their is another branch testing for fish.
c
the other problem with what you are asking for is the matching expression is in list format, if you were saying it it’s both then sure would have to be animal but then why not just say that?
p
@Anders Mikkelsen I don't think this would be hard to support. After all,
listOf(Cat(), Dog())
has inferred type
List<Animal>
today.
👍 2
@codeslubber I don't follow
c
if you say ‘is Cat, is Dog’ that syntax implies either, if you said ‘is Cat && is Dog’ then being handed an
animal
would make sense
p
it makes sense the way I proposed it. either of them is an animal; a cast to
Animal
will always succeed.
a
My point i guess, is that they are just as much Any, as Animal, or indeed any other interface or class in the hierarchy. So ofc it works for this simple example but can you generalise it?
Another way of looking at it is passing the any into a function, with a contract the expects an Animal
I suppose that would solve your annoyance, but kl ot exactly clean code
c
well also, isn’t it the case that parametric polymorphism is usually where pattern matching is used, so OOP style inheritance does not generally apply..
💯 1
b
A similar example:
Copy code
sealed class Animal {
    object Dog : Animal()
    object Cat : Animal()
}

val animal: Animal = Animal.Dog
when (animal) {
    is Animal.Dog -> animal
    else -> animal //This could be inferred to be a Cat, but instead stays as an Animal.
}
It seems the philosophy for these compiler inferences is that it’s performed iff there is exactly one type identifier on that line
t
The compiler could know that the value is an Animal, but it could also know that it's any supertype of Cat and Dog, and I could imagine the type inference required to support that kind of reasoning ending up in a nasty combinatorial explosion at times.
p
it seems logical for this to work given that
listOf(Cat(), Dog())
has inferred type
List<Animal>
. same thing really.
@Tim McCormack @Anders Mikkelsen this is called a least upper bound, and it's already used in other places (see my
listOf
example).
@Barco it's a limitation, not a philosophy. the problem isn't specific to sealed classes, which is why I changed my example to
open
.
b
I can think of a drawback when using non type-projected generic containers:
Copy code
sealed class Animal {
    object Dog : Animal()
    object Cat : Animal()
}
class DogContainer<T> : Container<T>

interface Container<T> {
    companion object {
        fun <T> just(element: T): Container<T> = DogContainer()
    }
}

val animal: Animal = Animal.Dog
fun getAnimal(): Container<Animal> {
    val newAnimal = when (animal) {
        is <http://Animal.Cat|Animal.Cat> -> Animal.Dog
        else -> animal //Inferring this to be a Dog would cause a compiler error..
    }
    val container = Container.just(newAnimal) //..as this container's parameter becomes Animal.Dog
    //Do  some other stuff with the container..
    return container
}
p
I don't understand. I never suggested type inference in the
else
branch.
b
I thought you are arguing for the case of type inference when a least upper bound is available in
when
statements in general. Perhaps I have misunderstood.
p
this doesn't apply to the else branch though. as I said, I'm not arguing for anything specific to sealed classes.
I'm just arguing that the same type inference should be used for
is Dog, is Cat ->
as for
listOf(Dog(), Cat())
.
I'm almost certain there's already an issue for it.
👍 1
a
Lets say Cat and Dog also implement the interface MouthBreather. What should be inferred? Animal or MouthBreather? And if you want to do animal operations on the object, why not is Animal?
✔️ 1
p
@Anders Mikkelsen I'll say it one last time: It should work the same as
listOf(Cat(), Dog())
does today. I've also answered the other question before.
a
Im not sure it works the way you think. Take this example:
Copy code
open class Animal
class Cat : Animal(), MouthBreather
class Dog : Animal(), MouthBreather
class Fish : Animal()

interface MouthBreather

fun main() {
    val someList = listOf(Cat(), Dog())
    val secondList = listOf(Fish())
    val list: List<Animal> = listOf(Cat(), Dog())
    val listTwo: List<MouthBreather> = listOf(Cat(), Dog())
}
In this case someList is of subtype Any, and secondlist is of subtype Fish. ie. is Cat, is Dog would still infer Any in this case, not animal. is Fish would tho, making the entire when pretty unpredictable unless you know the entire hierarchy.
p
good point. perhaps this isn't supported because a stronger type system (e.g., with intersection types) is needed to make it widely useful.
a
Probably, incidentaly Wharton had an interesting talk on kotlinconf about patternmatching thats upcoming in Java at some point. Would probably help you with this case. No idea if/when in regards to Kotlin tho. Might be interesting to check out if you havent seen it.
p
thanks I'll have a look.
in fact,
when
already behaves the same as
listOf()
, and as you stated, the whole thing breaks down when the classes have interfaces in common. 😞
a
Quite right
p
the type should really be
Animal & MouthBreather
, but Kotlin's type system can't express that.
👍 3
too bad, I'll stick to casting then
👍 2
d
@pniederw your original code snippet with multiple
is
in
when
condition is known problem of current data flow analys it can intersect smartcast infos from
cond1 && cond2
, but can't union them from
cond1 || cond2
this problem will be fixed in new compiler that was announced on kotlinconf
💯 2
the type should really be
Animal & MouthBreather
, but Kotlin's type system can't express that.
actually, with enabled new inference algorithm you can use implicit intersection types This is valid code with new inference:
Copy code
interface A {
    fun foo()
}

interface B {
    fun bar()
}

interface C : A, B
interface D : A, B

fun <K> select(x: K, y: K): K = x

fun test(c: C, d: D) {
    val x = select(c, d) // x: {A & B}
    x.foo()
    x.bar()
}
but there is limitation that intersection types can not be writen explicitly, so they can appear only in local declarations