3 ms·
I've thought about this a good amount too because Typescript gets so much out of untagged unions but I think that with a language like Rust you get into a huge
by rtpg 1y ago
I've thought about this a good amount too because Typescript gets so much out of untagged unions but I think that with a language like Rust you get into a huge mess due to it messing up inference and also Rust _really_ wanting to know the size of a type most of the time.
let mut x = 1
x = false
is x a usize? a bool? a usize | bool?
let mut x = if some_condition { 1 } else { false }
is x x a usize? a bool? a usize | bool?
One could make inference rules about never inserting unions without explicit intervention of user types. But then you get into (IMO) some messy things downstream of Rust's pervasive "everything is an expression" philosophy.
This is less of a problem in Typescript because you are, generally, much less likely to have conditionally typed expressions. There's a ternary operator, but things like `switch` are a statement.
So in production code Typescript, when presented with branching, will have a downstream explicit type to unify on. Not so much in Rust's extensive type chaining IMO. And then we start talking about Into/From and friends....
I don't really think that you want a rule like `(if cond { x: T } else { y: U}) : T | U` in general. You'll end up with _so many_ false negatives and type errors at a distance. But if you _don't_ have that rule, then I don't know how easily your error type unification would work.
- _benton 1y agoWell, let x = 1; x = false; is a type error in TS anyways. let x: Number | Boolean = 1 is fine but also clear about its allowed types. Cant you do something like let mut x: Result<Either<Foo, Bar>, Error> in Rust? Same thing, just more ceremony?
- rtpg 1y agoYeah my point is more about in type inference. If you explicitly annotate the expression's type then I'm not worried. Just like.... if you infer the union then all your type errors are going to shift around and you'll have to do more hunting to figure out where your stuff is. And my impression is that Rust has a lot more expression inference going on in practice than TS. But just an impression.
- madeofpalk 1y agoI don't understand the ambiguity. let mut x = 1 x = false In TS, x is inferred as usize, second line is an error. let mut x = if some_condition { 1 } else { false } In TS, x is inferred as usize | bool. Is there something specific to rust that makes this less clear that I'm missing?
- rtpg 1y agoIn TS you get literals, which is its own dimension of valuable tooling of course. So inferring as an untagged union is not wrong of course! It's just that if you are always inferring the type of an if expression to A | B, then this will also happen unintentionally a lot. And so at the end of some code, when you actually use x, then you'll see an error like "expected usize, got usize | bool". In the case that this was a mistake, you're now looking at having to manually figure out why x was inferred this way. In typescript your "if expression" is a ternary expression. Those are quite rare. In rust they're all over the place. Match statements are the same thing. Imagine having a 10 clause match statement and one of them unintentionally gives a different type. There's even just the classic "semicolon makes the branch into a ()"! So always inferring a union across branches of an if expression or a match means that your type errors on genuine mistakes are almost never in the right spot. Of course we can annotate intermediate values to find our way back. Annotating intermediate values in a chained expression is a bit miserable, but it is what it is. Decent typescript tends to not have this problem because there are few syntactic structures where you need to evaluate multiple branches to figure out the type of an expression. And the one big example (return values)... well you want to be annotating the return value of your functions in general. Rust is in a similar space for Result types, at least. But I don't think it generalizes at all. If you start inferring union types, and combine that with trait resolution, I _think_ that we'd end up with much less helpful error messages in the case of actual mistakes, because the actual location of the error will be harder to find. let mut x = if some_condition { 1 } else { false } // bunch of code return f(x) // expected usize, got usize | bool TS gets away with this stuff because JS's object model is simple (TS doesn't need to do any form of trait resolution!) and the opportunities to introduce unions implicitly are relatively few in TS code in general. And this isn't even really getting into Rust needing to actually implement untagged unions if they had them! Implicit tagging feels off in a language very serious about not having expensive hidden abstractions. But how are you going to guarantee bit layout to allow for the differentiation here? I'm saying all of this but I'd love it if someone showed up with a good untagged union proposal to Rust, because I _like_ the concept. Just feels intractable