I think union types can be implemented in Kotlin not by changing the type-system, but by transforming at compile-time union declarations into sealed classes.
I’ve been using sealed classes hierarchies to emulate union types (because I found them to be extremely useful in practice) but it’s very inconvenient to create type hierarchies all the time. Union types can be seen as simple ad-hoc sealed class hierarchies.
Suppose we want to emulate Either
.
Currently, we need something like this:
sealed class Either<out S, out F>
{
data class Success<out S>(val value: S) : Either<S, Nothing>()
data class Failure<out F>(val failure: F) : Either<Nothing, F>()
}
Which can be used as:
val e: Either<String, Exception> = Either.Success("hi")
val a: Either<String, Exception> = Either.Failure(Exception("error"))
val s: String = when (e)
{
is Either.Success<String> -> e.value
is Either.Failure<Exception> -> e.failure.toString()
}
If Kotlin supported union types, we could just do:
val e: String|Exception = "hi"
val a: String|Exception = Exception("error")
This is kind of the same as Optional
VS null, null is better in Kotlin as you don’t need to wrap it into something and is just as safe because the compiler checks nullable types.
The when
block would work the same. The Either
type could be generated automatically as Union2<A, b>
or something.
This gives the same power as sealed classes but avoid us having to create strict hierarchies every time we need something like this (which in my experience is a lot).
Intersection types are also really helpful when you try to model some complex domains. For example, I am now trying to model items which may or may not appear in a List. But some of them may or may not appear also in an enumeration (but the set of types that can go in a List is not exactly the same as the types can be part of an enumeration). So currently I need to model this using sealed classes + interfaces, but the interfaces cannot express that implementations must be one of the sealed class members, so modelling this reasonably in Kotlin is impossible. If Kotlin had intersection types, that would be trivial:
interface CanBeListItem
interface CanBeEnumeration
sealed class Item {
data class A(..): Item(), CanBeListItem
data class B(..): Item(), CanBeEnumeration // some types can be both
data class ListItem(itemsType: Item & CanBeListItem): Item()
data class EnumItem(enumsType: Item & CanBeEnumeration): Item()
}
The above is a oversimplification, but I hope my point comes across. I don’t think these use-cases are edge cases, I come across this kind of thing quite often! Hope others agree with me and you consider this option.