damian
07/31/2024, 2:22 PMCLOVIS
07/31/2024, 2:32 PM"ad-hoc unions" with erasure. Is something like this still under consideration for Kotlin?No, because: • having ad-hoc unions makes the typesystem much much more complex, which will make the debugger much slower than it is • erasure means no interoperability with Java
damian
07/31/2024, 2:35 PMhaving ad-hoc unions makes the typesystem much much more complex, which will make the debugger much slower than it isI think there are solutions to this, right? Maybe making them less useful, but you can limit the amount of inference involved and require explicit types when unions are involved, and so forth. Why does C# not consider this a problem?
erasure means no interoperability with JavaTrue, but I think we're at a point where that's not as big of a concern. Inline value classes, coroutines, and so forth also have this problem but the idea there is that you don't use those in Java-facing APIs.
CLOVIS
07/31/2024, 2:36 PMWhy does C# not consider this a problem?No idea, but the Kotlin team has said multiple times that this is the main blocker.
Ben Woodworth
07/31/2024, 6:39 PMmikhail.zarechenskiy
08/06/2024, 10:59 AMhaving ad-hoc unions makes the typesystem much much more complex
Maybe making them less usefulExactly. But to do so, we have to be sure about our use cases. It's easy to bring a feature to the language, but it's way harder to find the right balance between complexity and usefulness; otherwise, we can get accidental complexity. What exactly do we want to make better with unions in a particular language? For now, in Kotlin, where we have overloads, defaults, sealed types, and type inference, I believe the best fit for unions is for handling errors, and this is the design we're exploring right now. But again, we have to discuss the whole picture of handling errors. What options do we have right now, how unions will integrate with the rest and so on. --- And to give you some intuition why unions aren't good for the type checker. The main problem is type variables. These are crucial for us because we want to write generic functions and reuse existing library methods for collections that work with union data types, for example. Let's imagine I get the following constraint in my system: X | Y <: A | B (<: means subtype) Where X and Y are type variables; A and B are regular types. In order to solve it, we have to simplify it first:
X <: A | B
Y <: A | B
=>
X <: A OR X <: B
Y <: A OR Y <: B
And here's the problem. 'X' is not a regular type but a type variable. So to fairly solve our constraint system, we have to try to fix the type variable X := A and try to solve the rest of the system. This is the so-called "fork point". After we solve the rest of the system, we have to go back and try to solve the system when X := B. And, of course, we have to fork the constraint system for 'Y' as well.
So, if the complexity for type checking of regular union types is O(n * m) (n — number of components on the left, m — components on the right), then the story with generics is way worse. Very-very rough we have to solve our constraint system m^type_vars times or m^n times here. Unfortunately, when m is higher than 1, it can already be painful, as solving the whole constraint system more than once and performing collateral computations can be too much