9 ms·
I agree with Rich Hickey here and I think this is a flaw of sum types compared to union types. If Option<T> was a union type, T would be a subtype of Option<T>
by GrumpySloth 3y ago
I agree with Rich Hickey here and I think this is a flaw of sum types compared to union types. If Option<T> was a union type, T would be a subtype of Option<T> and those changes wouldn't be breaking. When writing Rust, I find myself repeatedly writing implementations of the From trait for enums, just because most of the time I actually want union types, not sum types. I think sum types should be built on top of union types by combining union types with a "lexically-scoped newtype", if that makes sense (i.e. they wouldn't be built in, they would be the result of two orthogonal features, while allowing for other combinations as well).
Edit: another nice consequence of this design would be None being its own type, which can be transparently converted to an Option<T> for any T, allowing for type inference to go in only one direction, while still avoiding the boilerplate of Option<T>::None. Unidirectional type inference would make for more intelligible compiler errors. Full Hindley-Milner can get confusing.
- gdprrrr 3y agoI use this crate to avoid the boilerplate https://jeltef.github.io/derive_more/derive_more/from.html https://jeltef.github.io/derive_more/derive_more/from.html
- mjburgess 3y agoWhenever i design a lang, i just give option semantics to types of the form `x|none`
- kibwen 3y ago> which can be transparently converted to an Option<T> for any T, allowing for type inference to go in only one direction, while still avoiding the boilerplate of Option<T>::None You don't need to qualify the None, the following works: fn foo<T>(x: T) -> Option<T> { None }
- yakubin 3y agoYes. That's thanks to Hindley-Milner type inference which makes the compiler infer which None you mean. But in longer code snippets full Hindley-Milner type inference can cause hard-to-understand type errors. Here specifically the compiler infers the type of a value in your function body based on the type of the function it belongs to, which is in the opposite direction than what a programmer typically thinks in. It leads to spooky actions at a distance.
- kibwen 3y ago> It leads to spooky actions at a distance. I'll disagree on "spooky". If the type is ambiguous, the code will fail to compile. Contrapositively, that means that if the code compiles, the types aren't ambiguous. I find bidirectional type inference to be absolutely lovely (or at least Rust's implementation of it), and I wouldn't give it up.
- stefncb 3y agoIt's actually not. There's simply a 'use Option::{None, Some}' built-in to make them easier to work with.
- estebank 3y agoThe GP is referencing cases where inference can't figure out what the T is in Option::<T>::None. This can happen in the body of a closure without an explicit return type, for example. To solve it you have to specify the type either earlier in a place that helps inference (in the example, add a return type) or in the expression, like None::<()>.
- stefncb 3y agoYeah sorry I apparently can't read, it was pretty obvious. Thanks.
- diarrhea 3y agoAs another poster said, None is in the prelude. It’s been explicitly imported, just… implicitly so. Other than that, Rust places strong emphasis on function signatures. They override everything else, such as inferred return types from the function body. This is to ease composition and reading (as far as I remember). That is to say, in Rust it is quite natural to refer back to the function definition, potentially to let it do inference heavy lifting, or work with the question mark operator. It doesn’t feel the wrong way.
- estebank 3y ago> But in longer code snippets full Hindley-Milner type inference can cause hard-to-understand type errors. I would love to see examples of this. We've gotten much better on this front over the years, but I'm sure there are plenty of cases yet to be addressed. The "problem" with improving error messages is that the common and easy cases are addressed early, leaving only the uncommon and difficult to address left after a while, and people get used to the understandable errors which leaves them baffled when they encounter one that isn't.
- lmm 3y agoUnion types are horrendous in practice because they're non-compositional. None|T is usually disjoint except when it isn't, and it's really easy to forget that case and fail to test it properly. Having used Scala extensively, None as its own type is very much a mistake; it's not a type that you ever want and it only serves to get in the way.
- tomjakubowski 3y agoWhat does it mean for None|T to be disjoint?
- lmm 3y ago"If it's None then it's not T". It's very natural to write code that handles the case where the value is None, handles the case where the value is T, but subtly malfunctions if T can be None (e.g. you might write a cache and use None to represent the value not being present in the cache - but then your cache silently fails to cache if the thing you were caching returns None).
- squirtlebonflow 3y agoHow is it natural to write code like that? If it's None, don't put it in the cache. If T is None, don't put it in the cache. if (typeof input != None) putInCache(input)
- lmm 3y ago> If it's None, don't put it in the cache. If T is None, don't put it in the cache. > if (typeof input != None) putInCache(input) Exactly, now you've just written exactly the bug I was talking about.
- nyanpasu64 3y agoIf T is int|None, then None|T can't distinguish between None and T(None), whereas None|Some(T) can distinguish between None and Some(T(None)).
- Hirrolot 3y ago> Unidirectional type inference would make for more intelligible compiler errors. Full Hindley-Milner can get confusing. Bidirectional type checking has good error messages and a very intuitive implementation.
- lolinder 3y agoRather than just taking a statement from OP and stating the opposite, can you elaborate on why you disagree with them? My personal experience has been very much in line with OP's: nonlocal type inference (in Rust specifically) frequently makes code hard to modify because a change can cause inference to fail in unexpected ways and places. Local inference doesn't have the same tendency.
- debugnik 3y agoThey didn't state the opposite, bidirectional typing rules are a technique to describe a type system, distinct from both Hindley-Milner's and simple "unidirectional" checking. I'm using it for my personal lang project and I'm quite satisfied as well, it translates easily into code and interacts well with subtyping.
- bsder 3y ago> most of the time I actually want union types, not sum types. Really? This surprises me to the point that I'd like to ask what you are coding. I'm having a very difficult time picturing "You have A|B|C|D and you send in an A and extract a D" (union type without tag). The only way to do that is to guarantee memory layouts, which is something Rust explicitly does not do. What am I missing?
- yakubin 3y agoYou're thinking about C unions, not union types. Union types behave more like unions in set theory. Sum types behave more like disjoint unions. In particular, if you have a union type A|B, then A is a subtype of A|B. If you have a sum type A+B, then A is not a subtype of A+B, because for A=B, A+B has two instances of A, i.e. A+A != A, so for each element you need to be able to distinguish which instance this element is from. Whereas with union types A|A = A. It doesn't mean that union types are as willy-nilly as unions in C. It just means that you can assign variables of type A to variables of type A|B without any ceremony. If you want to extract an A from A|B, you need to write a runtime check (just like match in Rust with sum types). It does mean though that you can assign a value of type B to a variable of type A+B and then extract an A, if that value was in the intersection of A and B.
- bsder 3y agoI understand both the mechanics and the implementation. What I was asking for was the use case. Making assignment easier and compile time at the cost of making access costlier and runtime seems like an unusual tradeoff. I'm very interested in the case that makes that worthwhile.
- codebje 3y agoTransferring costs from compile time to runtime is pretty much the signature move of dynamic languages. If you already carry around type information and check it at runtime, why not just add union types? Your implicit conversion is zero cost at that point, after all.
- 3y ago