```sealed class Either<out L, out R> private...
# getting-started
m
Copy code
sealed class Either<out L, out R> private constructor() {
    class Foo<out L>(val foo: L): Either<L, Nothing>()
    class Bar<out R>(val bar: R): Either<Nothing, R>()
}

sealed class Data private constructor() {
    class A(val int: Int): Data()
    class B(val string: String): Data()
}

(Either.Bar(Data.B("")) as? Either.Bar<Data.A>)?.let {
    println("Either.Bar(Data.B(\"\")) is Either.Bar<Data.A>")
}
Why is the
println
statement executed?
c
Type parameters only exist at compile-time. At runtime, they don't exist at all. So
as Either.Bar<Data.A>
is actually
as Either.Bar
. And that's true, it's an
Either.Bar
! So the cast is successful. You should be getting a warning in your code:
If you follow the quickfix by the compiler, it replaces
Either.Bar<A>
by
Either.Bar<*>
, which makes it explicit that the type parameter is not verified
Copy code
(Either.Bar(Data.B("")) as? Either.Bar<*>)?.let {
        println("Either.Bar(Data.B(\"\")) is Either.Bar<*>")
    }
m
I did get that error but I didn’t understand why it kept saying I wasn’t checking when I clearly checked
c
It's not you, it's the compiler. It is not possible for the compiler to verify this test, even if you write it in your code it does nothing
m
It’s quite shocking that this compiles at all
c
It could be argued that it should be stronger than a warning, yeah
m
Well this defeats the entire point of using
as?
as opposed to
as
in this particular case
c
Right, but
as
was not a good idea in the first place here
Copy code
sealed class Either<out L, out R> private constructor() {
    class Foo<out L>(val foo: L): Either<L, Nothing>()
    class Bar<out R>(val bar: R): Either<Nothing, R>()
}

sealed class Data private constructor() {
    class A(val int: Int): Data()
    class B(val string: String): Data()
}

fun main() {
    val b = Either.Bar(Data.B(""))
    if (b.bar is Data.A) {
        println("Either.Bar(Data.B(\"\")) is Either.Bar<A>")
    }
}
Not only is the code simpler, but the compiler actually refuses to compile because it sees that it's dead:
Copy code
Check for instance is always 'false'.
m
I only tried that because
Copy code
when (/* ... */) {
    is Either.Bar<Data.B> -> /* ... */
    // ...
}
This didn’t work. I was exploring alternatives
c
Same problem, and you'll get the same warning,
is Either.Bar<Data.B>
is just
is Either.Bar
A test of the form
is Foo<Bar>
is always just
is Foo
, that's a limitation of the execution environment (not possible to do a real test on the JVM nor on JS)
m
I’m stumped.
Copy code
fun main() {
    for (either in listOf<Either<Boolean, Choice>>(
    	Either.Foo(true),
        Either.Bar(Choice.Case0(42)),
        Either.Bar(Choice.Case1("")),
    )) {
        either.fooAs<Boolean>()?.let { foo ->
        	println("Either.Foo(${ foo })")
        }
        either.barAs<Choice.Case0>()?.let { bar ->
        	println("Either.Bar(Choice.Case0(${ <http://bar.int|bar.int> }))")
        }
        either.barAs<Choice.Case1>()?.let { bar ->
        	println("Either.Bar(Choice.Case1(${ bar.str }))")
        }
    }
}

sealed class Either<out L, out R> {
    class Foo<out L>(val foo: L): Either<L, Nothing>()
    class Bar<out R>(val bar: R): Either<Nothing, R>()

    inline fun <reified T> fooAs(): T? {
        return (this as? Either.Foo<*>)?.foo as? T
    }
    inline fun <reified T> barAs(): T? {
        return (this as? Either.Bar<*>)?.bar as? T
    }
}

sealed class Choice private constructor() {
    class Case0(val int: Int): Choice()
    class Case1(val str: String): Choice()
}
This is typesafe but no longer exhaustive.
c
Yeah, don't write that
Copy code
fun main() {
    for (either in listOf<Either<Boolean, Choice>>(
    	Either.Foo(true),
        Either.Bar(Choice.Case0(42)),
        Either.Bar(Choice.Case1("")),
    )) {
        when (either) {
            is Either.Foo if either.foo is Boolean ->
                println("Either.Foo(${ either.foo })")

            is Either.Bar if either.bar is Choice.Case0 ->
                println("Either.Bar(Choice.Case0(${ <http://bar.int|bar.int> }))")

            is Either.Bar if either.bar is Choice.Case1 ->
                println("Either.Bar(Choice.Case1(${ bar.str }))")
        }
    }
}
Simpler, no need for
reified
, and it is exhaustive
m
Oh, a
when
predicate can include an
if
clause?
Copy code
fun main() {
    for (either in listOf<Either<Boolean, Choice>>(
    	Either.Foo(true),
        Either.Bar(Choice.Case0(42)),
        Either.Bar(Choice.Case1("")),
    )) {
        println(when (either) {
            is Either.Foo -> "Either.Foo(${ either.foo })"
            is Either.Bar if either.bar is Choice.Case0
            	-> "Either.Bar(Choice.Case0(${ either.bar.int }))"
            is Either.Bar if either.bar is Choice.Case1
            	-> "Either.Bar(Choice.Caes1(${ either.bar.str }))"
        })
    }
}
This fails to compile; I get the following error.
Copy code
'when' expression must be exhaustive. Add the 'is Bar<*>' branch or an 'else' branch.
What am I doing wrong?
c
Can you ask IntelliJ to infer the type of
either
?
image.png
I think it should realize that
either.bar
is a
sealed class
so all branches are covered
m
@CLOVIS Do you mean this?
c
yeah. I'm not sure why it doesn't recognize the sealed class in this situation, I think it should
In the meantime you can write it like this:
Copy code
for (either in listOf<Either<Boolean, Choice>>(
        Either.Foo(true),
        Either.Bar(Choice.Case0(42)),
        Either.Bar(Choice.Case1("")),
    )) {
        println(
            when (either) {
                is Either.Foo -> "Either.Foo(${either.foo})"
                is Either.Bar -> when (either.bar) {
                    is Choice.Case0 -> "Either.Bar(Choice.Case0(${<http://either.bar.int|either.bar.int>}))"
                    is Choice.Case1 -> "Either.Bar(Choice.Caes1(${either.bar.str}))"
                }
            }
        )
    }
but I'm pretty sure the compiler should realize it's the same code
y
From the when guards KEEP:
Given that guards may include any expression, branches including a guard should be ignored for the purposes of exhaustiveness. If you need to ensure that you have covered every case when nested properties are involved, we recommend using nested conditionals instead.
```// don't do this
when (status) {
is Status.Error if status.problem == Problem.CONNECTION -> ...
is Status.Error if status.problem == Problem.AUTHENTICATION -> ...
is Status.Error if status.problem == Problem.UNKNOWN -> ...
// the compiler requires an additional 'is Status.Error' case
}
// better do this
when (status) {
is Status.Error -> when (status.problem) {
Problem.CONNECTION -> ...
Problem.AUTHENTICATION -> ...
Problem.UNKNOWN -> ...
}
}```
m
TL;DR version why it doesn't work: a general form of understanding if your cases + if-guards are exhaustive is equivalent to SAT-solving, and we don't want to be like Rust and do NP-hard problems during compilation. Adding special cases for "well-defined if-guard shapes" would mean that some slight changes to your code suddenly break the special-casing and make your code red. And we don't want that either.
🤔 1
m
That’s unfortunate. I didn’t expect tagged unions to entail an overwhelming amount of friction.
m
In a language with erased generics? That's unfortunate but expected.