-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RFC: Anonymous enum types called joins, as A | B
#402
Conversation
Add support for defining anonymous, enum-like types using `A | B`.
Note that this plays very well with #243 by allowing |
I would very much like to see union types (why do you call them join types BTW?) in Rust! Ceylon language is probably the nicest example in existence of their implementation. |
While I totally agree this is a useful feature for errors, it's a major change to the type system, and one that introduces a bunch of machine representation issues, especially if Seperate to that worry, I'd recommend taking a look at OCaml's "polymorphic variants" for inspiration, which is a neat idea similar to this. |
@Ericson2314 By "coerces to" I meant that the compiler will implicitly insert the translation, not that they have the same representation. Notably, it is not safe to transmute from @netvl the above is related to why I did not call this |
@netvl Wow, Ceylon's union type is pretty cool! It is used in ways I wouldn't anticipate this being used in Rust though, i.e. I wouldn't necessarily expect |
"By "coerces to" I meant that the compiler will implicitly insert the translation". Ok, that's a step in the right direction IMO. If Edit: Making |
@reem, well, "union types" is an established term for such kind of types, AFAIK. It is a bikeshed, however, I agree, I just wanted to clarify it :) Union types in Ceylon (well, in just about any language on JVM, and not only union types - all types) rely much on subtyping. In Rust subtyping is almost nonexistent, and introducing it not only makes the type system much more complicated, there would also be no natural way to create subtypes in absence of inheritance or something like it. So it makes sense to treat union types just as anonymous enums, just as like you suggest. After all, closures already are generating anonymous structures. I don't really know how the compiler operates, but I think this can be made in a similar way. |
In the same vein, multiple occurrences of `A | B`, even in different crates, | ||
are the same type. | ||
|
||
As a result of this, no trait impls are allowed for join types. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not? Trait impls are allowed for tuples, which are anonymous structs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually I had not thought this through. My main motivation in saying this was that you can't write an impl for a type or trait you didn't define, but with the new rules only one of the types in the union has to be yours. We could use exactly the same rules as with tuples.
Is |
I'd prefer if not. That would make no sense and be just sad |
The motivation mentions error handling but this problem can also be resolved in a different way through #201. |
@flying-sheep why does it make no sense? |
If a flat union |
@kennytm In the same vein, My reasoning for this is that let t = T as T | T; // ambiguous as to which anonymous variant is instantiated
match t {
T => {} // ambiguous as to how this is matched
} |
@reem You cannot disallow that because:
Also disallowing fn perform_either<T, A, B>(a: || -> Result<T, A>, b: || -> Result<T, B>) -> Result<T, A | B> { ... }
perform_either(
|| rename_file(),
|| copy_and_delete_original_file(),
);
// Will get `IoError | IoError` here. |
A previous proposal for this supported |
I think it's possible to resolve unambiguously without positioning if we just make |
It's not shorthand for We could also have "do what I mean": (This is true even with generics that can be instantiated to be the same type, since the point-of-coercion just sees the two distinct generic types, it does not care that they could end up both being |
I can't really think of a situation where I'd care about which variant was chosen out of |
I kinda prefer this over #201 |
There are a lot of uses of union types outside of error handling
should be equal to
You could possibly allow coercing a union type to another union type during match. |
As before I think we should clearly distinguish between anonymous sum types and union types, at least to avoid confusing ourselves. Both are potentially useful, they are vaguely similar but actually quite different, and the main thing they have in common is that both want the same syntax. Anonymous sum types would be nothing more than special built-in syntax for Union types would be a kind of type-based union which essentially has one variant per type, rather than "positional" variants. This may also allow automatic conversions from (Maybe more later, only had time for this right now.) |
This is an interesting idea but there are a number of subtleties involved that I'm not sure are fully addressed by the current proposal. Some care should be taken as there are non-obvious issues with an ad-hoc approach that can lead to unsoundness and other problems. It would be useful to understand this proposal in relation to similar ideas elsewhere, like extensible/polymorphic variants in OCaml, extensible exceptions ( I'm also concerned that interaction with generics will lead to serious usability issues without something like row-polymorphism and/or subtyping (like OCaml). I don't know enough about Rust's internals to have a good idea whether either of those would be feasible here but neither are trivial additions in most cases. |
Let me leave in an argument for T | T becoming T:
|
I've thought a bit about the duplicate type problem, and I want to articulate my new thoughts: We must support duplicate types in joins, at minimum when instantiated through generics. Let's take an example: fn try_both<A, B>(a: || -> Option<A>, b: || -> Option<B>) -> Option<A | B> {
a().unwrap_or_else(|| b());
} This function must work when instantiated with If we assert that in the case where EDIT: Effectively, if you instantiate the above method with the same type parameter, the type of the above function would be |
I do not like this idea. I am seeing comments like "I don't care about this case" in response to issues with generics, and that makes me think this isn't well-thought out. What does this actually do that the existing type system cannot already do? Is this going to be a weird wart on Rust's type system? I think any solution like this must be motivated by existing problems with more than syntax. For example, if there were a way that this could lead to collapsed enum representations for repeatedly nested types, that would be a win in my view. But it is not clear to me that this proposal attempts this, which again leads me to feel that it isn't that well thought-out. |
If you think of Also, if you want |
@zwarich You've pointed out an important issue. I've given this some more thought and I'm now actually unsure if @pythonesque This is a large and important feature, so I certainly think further consideration is warranted, but I also think that this, in some form, is a feature that we will likely want as it vastly simplifies many common cases. |
In the triage meeting today we decided to close this RFC and not merge it, as it is lacking some critical details with no single obvious solution. We did agree that this space of ideas has some compelling use cases, so I opened an issue in the RFCs repo to track looking into it further: #409. |
As others have noted this seems like a small and obvious feature to improve convenience and ergonomics at first, but it's actually a pretty big one with potentially profound implications for the type system. At least in its most general form. With respect to representation, my feeling is that the correct one would likely be Here are the tricky questions I can think of. Most of them have already been posed (and often answered in a way) in the RFC and comments, but just to gather them in one place. I'm going to use a variadic
In the short term, a pragmatic approach might be to just forbid most of the things, e.g. to disallow type variables (and possibly trait objects as well) from appearing in unions, which would avoid many, but not all, of the difficulties (but also be less useful). In the longer term it's probably best to study the literature, such as linked by @darinmorrison. But all in all I'm not sure that all this machinery would be worthwhile for what is in the end a "constant-factor" improvement in convenience, rather than an increase in expressiveness or abstraction power.
This comment appears to be at odds with my previous sentence. My impression was that the main benefit of this feature would be being able to avoid a bunch of |
I have tried my hand at making a new version of this RFC that tries to address some of the concerns with it. Do you think it's worth submitting a pull request? |
@scialex I haven't read the entire thing yet, but does it propose a compilation strategy? |
@zwarich not explicitly. I do not think it really needs a huge one. The compiler figures out all the overlaps that could occur given the type information it has, creates an enum with one varient for each containing a type of the given combinations. matches and all impls are just desuggared to use this. We have type mono-morph so it should be guaranteed to work (I think, its been a while since compilers and I haven't taken a very deep look at rustc internals). This does rely on being willing to have the in memory layout of ie Also this could cause perf problems with large numbers of very generic types but that doesn't seem to be the main usecase. IE this would take a lot of time/memory fn stuff<A, B, C, D, E>(a: &A, b: &B, c: &C, d: &D, e: &E) -> &(A|B|C|D|E) { ... } since it would be really returning a 120-variant enum. |
your new RFC is great! i’m desperately missing something like join types in my attempt to create a typesafe document tree (the children of one type can be of one of several other types, e.g. |
@scialex I like some of the changes in the RFC, but I think the treatment of duplicate types could use a lot of work, since it makes using variants of more than 2 extremely unergonomic - As a result, I don't think that RFC should be used as-is. This has sort of slipped my mind in the past weeks, but I think I may take another stab at significantly clarifying many of the edge cases and important ideas brought up here. |
Now that we have the // implement FromError for LibError.
enum LibError { ... }
impl FromError<ErrorX> for LibError { ... }
impl FromError<ErrorY> for LibError { ... }
// then just use `try!` as is from now on.
pub fn some_other_operation() -> Result<(), LibError> {
let x = try!(produce_error_x());
let y = try!(produce_error_y());
Ok(())
} |
@flying-sheep: Cool that's actually almost the exact same reason why I wanted to revive it. I am making a file-system. stuff can return @kennytm: Added note about FromError. @reem: Updated the RFC to address that concern, making it easier to match on large unions. I have improved it some and am going to submit it. Submitted pull request #514 |
We can define anonymous enums as follows: We can match on them as follows:
As for any other types we can implement traits for them. |
auto_enums crate provides this now. |
Add support for defining anonymous, enum-like types using
A | B
.Rendered