iex
10/29/2025, 2:32 PMsealed class Foo {
data object Foo1 : Foo()
}
sealed class Bar {
data object Bar1 : Bar()
}
fun abc() {
val foo: Foo = Foo.Foo1
when (foo) {
Foo.Foo1 -> {}
Bar.Bar1 -> {} // <-- invalid type!
}
}Youssef Shoaib [MOD]
10/29/2025, 2:35 PMis checks. Hence, a rogue implementation of Foo can make itself always equal to Bar1. Obviously, Foo is sealed, and no implementation of it has any such strange equality, but the compiler is not aware of that I guess. If you write this with is checks, you do get an error:
sealed class Foo {
data object Foo1 : Foo()
}
sealed class Bar {
data object Bar1 : Bar()
}
fun abc() {
val foo: Foo = Foo.Foo1
when (foo) {
is Foo.Foo1 -> {}
is Bar.Bar1 -> {} // Check for instance is always 'false'.
}
}
Such strange equals implementations are still respected by the compiler, perhaps because in other situations they can make sense (different Set implementations compare equal still, so 2 objects being equal doesn't imply they have the same type)iex
10/29/2025, 2:37 PMis there's a warning (not error). Maybe I should use always isYoussef Shoaib [MOD]
10/29/2025, 2:37 PM2.3.0-Beta2. At least when testing in playgroundiex
10/29/2025, 2:38 PMiex
10/29/2025, 2:38 PMYoussef Shoaib [MOD]
10/29/2025, 2:39 PMiex
10/29/2025, 2:40 PMis vs skipping it?Youssef Shoaib [MOD]
10/29/2025, 2:42 PMequals implementation. It used to be the case that is was always the correct way to do it, but then we got `data object`s, whose equals implementation just uses is internally.
Personally, I still like the is. There's no drawback AFAIKYoussef Shoaib [MOD]
10/29/2025, 2:42 PMiex
10/29/2025, 2:44 PMiex
10/29/2025, 2:44 PMis to everythingYoussef Shoaib [MOD]
10/29/2025, 2:46 PMiex
10/29/2025, 2:46 PMiex
10/29/2025, 2:55 PMelse clause. Otherwise you'd notice via the missing casesYoussef Shoaib [MOD]
10/29/2025, 3:03 PMelse this might be intentional behaviour, though, or at least difficult for the compiler to track. I'm guessing you mean a case like:
sealed class Foo {
data object Foo1 : Foo()
}
sealed class Bar {
data object Bar1 : Bar()
}
fun abc() {
val foo: Foo = Foo.Foo1
when (foo) {
Bar.Bar1 -> {} // <-- invalid type!
else -> {}
}
}
It's difficult to accurately produce a warning here. The compiler would have to determine all possible Foo subtypes, and then know that they don't override equals in any crazy way.iex
10/29/2025, 3:30 PMelse (I try to do exhaustive checking everywhere but here's just a very long list and am interested in only 2 of them), and well after the refactoring the paywall was not being triggered anymore and no idea whyiex
10/29/2025, 3:30 PMYoussef Shoaib [MOD]
10/29/2025, 3:32 PMis for now might be the best workaround.Youssef Shoaib [MOD]
10/29/2025, 3:45 PMequals business I mentioned above, but it thus should try to warn in cases where it makes sense, like here.iex
10/29/2025, 3:46 PMYoussef Shoaib [MOD]
10/29/2025, 3:47 PMYoussef Shoaib [MOD]
10/29/2025, 3:53 PMequals definitely not being overridden as a worthwhile additioniex
10/30/2025, 5:58 AMwhen {
this is ApiClientError.Api && (error == RawApiError.UserNotFound || error == RawApiError.ExpiredProduct) -> {
// this is never activated now
log.t { "Polling iteration: user not found or expired" }
// ...
}
else -> {
log.e(pay) { "Subscription polling unsuccessful" }
// ...
}
}
The refactoring was renaming previous ApiError into RawApiError, then changing the error in ApiClientError.Api to reference a new ApiError
sealed class ApiClientError {
data class Api(val error: ApiError) : ApiClientError()
// ...
}
sealed class ApiError {
data object ExpiredProduct : ApiError()
data object UserNotFound : ApiError()
// ...
}
sealed class RawApiError {
data object ExpiredProduct : RawApiError()
data object UserNotFound : RawApiError()
// ...
}