Union types (e.g. val x: Int | String) can be pretty finicky in variables, since you have to allocate memory for the largest type it could be, and also there’s the fact that the JVM is not designed for dynamic typing.
However, I think there’s a good compromise: only add union types for use in function headers as a shorthand for overloaded methods. The requirements would be that it is deterministic which type leads to which output, which pairs very well with the when block.
The reason I prefer this to the sealed typealias solution discussed in Union types is because it would allow an idiomatic generation of function overloads without requiring that you assign a new typealias to do so. However, I think the combination of the two features would be amazing for understanding code at face value, with the sealed typealias leading well towards an easy “deserialize” method for data that could come in one of many different types while making it clear what kind of data is being expected to be passed.
Examples:
Current Syntax:
fun foo(x: Int): Int = x + 2
fun foo(x: Double): Double = x + 2
New Syntax:
fun foo(x: Int | Double): Int | Double = x + 2
Example 2:
fun foo(x: List | String): Int {
when (x) {
is List -> return x.size
is String -> return x.split(";").size
}
}
Example 3:
Current Syntax:
fun printData(item: Any): Unit {
when (item) {
is Block -> print(item.blockData)
is Item -> print(item.itemData)
else -> throw Exception() // This is an unnecessary risk when all you want is short overloads
}
}
New Syntax:
fun getData(item: Block | Item): Unit {
when (item) {
is Block -> print(item.blockData)
is Item -> print(item.itemData)
}
}
Which would be compiled as:
fun getData(item: Block): Unit = print(item.blockData)
fun getData(item: Item): Unit = print(item.itemData)
It’s just an example; in reality I’m dealing with much larger blocks of code required. The point is that it would be relatively-valid syntax and would allow easier understanding of code than having several overloads.
Well, that is actually debatable. Looking at example 2 and 3, I would say the current versions are much more readable and easier to understand - your syntax is just very verbose:
fun foo(x: List | String): Int {
when (x) {
is List -> return x.size
is String -> return x.split(";").size
}
}
vs. currently
fun foo(x: List): Int = x.size
fun foo(x: String): Int = x.split(";").size
If the blocks become more complicated, your new syntax would become even more difficult to understand while the current version stays as clear as possible. In general, small local complexity is a good thing for understandability. Mixing in two/multiple different things into one method, increases local complexity by unnecessary coupling - thereby making the code more difficult to understand.
Example 1 actually could reduce some code duplication, but it looks like it should be done with some reasonable interface:
fun <T : Addable> foo(x: T): T = x + 2
The problem with that is that one cannot add conformance to an interface to an existing type - but even in your proposed syntax the compiler would somehow need to check that all subtypes of the union type “have the needed operations” (which is basically the definition of what an interface is for). So, if the compiler can support your syntax, it most likely could also support “on-the-fly” interfaces that can be conformed by already existing types by just having the right methods.
Adding to what @tlin47 already said, I would argue that even with the proposed feature, good coding style would actually warrant extracting the different branches of the when into separate functions.
Interestingly, this brings right back to just declaring the overloads.