-
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
Disjoins (anonymous enums) #1154
Conversation
While the syntax looks cool, I think using pipes is way too ambiguous. In a tuple definition, the only time you need to use parenthesis is to define a tuple field that is also a tuple. In your disjoin syntax, you need parenthesis for any expression using a pipe (or infinite lookahead and some really weird operator precedence rules), any inner tuple, and any inner disjoin. Bikeshedding-time: Wouldn't it make more sense to change it to look exactly like tuples, but just adding the let foo: enum (char, i32, i32) = enum(!,!,123); |
|
||
let foo: ! = panic!("no value"); | ||
match foo { | ||
}; |
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.
this'll cause the unreachable lint to warn. I'm not sure what this means for generics.
fn test<T>(f: &Fn() -> T) {
let x = f();
drop(x); // unreachable if T == !, but no useful operations are possible on T anyway?
}
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.
If this is an issue then it's also an issue with enum Never { }
which you can already write. (I don't believe there's any issue.)
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.
the match is reachable for enum Never { }
, it just is a noop. But for a diverging function it makes no sense to match on it's result, and if it did, you would never reach the match
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.
the match is reachable for enum Never { }
No it's not. enum Never {}
can never exist so a function that returns it is a diverging function.
But for a diverging function it makes no sense to match on it's result
Yes it does. A diverging function doesn't return a value so its return type is the type of no values which can be matched with an empty match statement. The following is currently valid rust code:
enum Never {}
fn foo() -> Never {
panic!("oh no");
}
fn main() {
let x = foo();
let y: String = match x {}:
}
Can you give a specific example of code that could be parsed ambiguously? I'm having trouble coming up with one.
That's not bad, it would be a little inconsistent to not also have tuples written |
that train has left the station with 1.0
let a = !;
let b = !;
let c = false;
let d = true;
let foo = (a|b|c|d); // proving or disproving this legal might end up being a hard problem let c = false;
let d = true;
let foo = (!|!|c|d|!); // unambiguous but error if disjoin pipes have precedence before bitor pipes Since
most of these are not true ambiguities, just really messy to perceive |
That's not strictly ambiguous as the only way to parse it is as |
oh... that makes sense... Wouldn't some other syntax for disjoin expressions make sense then? Something that directly uses the index and thus prevents These are all horrible but you get the picture:
|
You would need something that expresses the number of positions aswell. Something like |
I'd let inference do that, you need to specify the type somewhere anyway... |
I think that making anonymous enum types, literals, and patterns all use the same structural, positional syntax, just like anonymous structs (tuples) do, is one of the biggest draws of this design. We could conceivably use a different placeholder than With respect to syntax ambiguity, I'm most concerned about disjunctive patterns, but again, haven't tried thinking it through. (Also FWIW, I'm not sure if introducing a new word "disjoin" for these brings any real benefit over just calling them "anonymous enums". Makes it sound kind of new and magical, like an interesting new concept to learn, when it's really kind of banal - just nicer syntax for an existing thing. Sure, this breaks symmetry with tuples which do have their own word, but it was there before us.) |
Well currently disjunctive patterns can't appear inside parenthesis so this would only be a problem if they were promoted to first-class patterns. Are there plans to do this?
Fair enough, neologisms are always a bit wanky. It was mainly suggested for symmetry with tuples and because I thought the name fits. |
👍 i really missed those in a typed document tree library. creating a |
well, see #402, there’s some prior discussion with interested people |
Note that this is explicitly a different (and simpler) idea than in #402 and friends. |
well, the only difference to be just as useful and convenient as #402 is to introduce syntax sugar for e.g. with but of course that’s not necessary for accepting this RFC first, which is already very useful as-is. |
btw., i propose the name it would fit even better than there though:
|
This syntax would be problematic with rust's parametric polymorphism. Consider this code generic in
Compilation would fail when
Are you suggesting we replace the current syntax for diverging functions |
good point! can you elaborate on the “ad-hoc polymorphism”?
not at all, just as a name that can be voiced. how do people call it right now? “bang”? “noreturn”? |
Personally (as I've mentioned before) I like "never". |
I'm not sure on the correct terminology, but I was refering to the difference outlined here. Basically, while C++ is statically typed the template meta-language is dynamically typed in that it just runs until it hits an error. By contrast, Rust's generics are typesafe (kindsafe? traitsafe?) in that the aformentioned code will compile for any
Ah okay. I've been calling it "bang" but "mu" is kinda cute. "Never" makes a lot of sense aswell. |
hmm, this is not an answer to “which return type has this function”, though 😉 i like “mu” so much because it’s something almost as useful as “yes” or “no”, but unavailable in the english language: “did you stop beating your children?” is a question most fittingly answered by “Mu” (I never started beating them, so neither ‘yes’ nor ‘no’ would give the right message)
so “constraint program” vs. “iterative expansion”.
makes sense: the code should compile if all syntax, trait and lifetime constraints are OK. on the other hand, my version would simply impose an additional “T: !String” trait bound, right? well, my variant is still useful when only used on non-generic types. |
Something like
That's true, but I'd be a little uneasy about introducing syntax that can only be used on non-generic types. |
"This function returns I agree that "mu" is also a nice fit on a purely theoretical level, it's just that most people haven't heard ever of it, so it doesn't really help them. |
-1 I understand why you would want this, but the problems it solves can be solved already with enums. Why cant we just do
It just seems like unncessary sugar. Are there any usecases for this that cant be accomplished already? |
In a recent lang subteam meeting, we decided to close this RFC (and list it under issue #294). While anonymous disjoint unions are interesting and have their uses, the advantages seem outweighed by the complexity introduced into the type system and runtime (as well as the duplication with existing enums). |
@nikomatsakis What do you mean by "complexity introduced into the type system and runtime"? This is almost entirely surface-level syntactic sweetener. Also, I seem to recall something in the RFC process to the effect that decisions should essentially just summarize arguments which were already discussed in the comments? (And shouldn't there have been a FCP, at least?) |
Sorry, my message was ill-phrased. I should have made it clear that the primary reason we decided to close this RFC was prioritization, not technical objections. There is only a certain amount of "conceptual bandwidth" for making changes -- and by this I mean the full package, so not just implementing but also communicating, understanding, documenting, maintaining the codebase, merging with other in-flight changes, etc -- and the consensus of the language subteam was that anonymous disjoint unions currently don't quite make the cut. Perhaps later, if there is a more pressing need, or when other changes have settled down.
We require an FCP before an RFC is accepted, but we frequently close RFCs during triage. This can either be for prioritization reasons (as in this case) or because we just don't feel that the RFC is something that we will ever want (not this case).
As I said, I didn't mean to express that technical objections were the reason for closing this RFC. Re-reading my message, though, I realize it really sounds that way. I apologize for that. That said, I do have some personal concerns with the RFC as written. For example, the RFC as written (at least, in the detailed design section) did not address coercion between union types, but it seems very surprising to me that |
@nikomatsakis Ah, okay. That makes perfect sense. So this is essentially a (re-)postponement?
I don't think we would want this kind of coercion any more than we would want a coercion from |
As a procedural note, the close-during-triage thing seems to be lang-team On Sun, Aug 2, 2015 at 2:42 AM, Andrew Cann notifications@github.com
|
If #1450 were to be accepted, is there any potential this could be revived? In particular, if one can only form disjoins of types that do not unify, all ambiguity can be avoided - disjoins could be declared as simply as struct Bar;
type MyDisjoin = Bar | [u8; 4]; assigned as let x: MyDisjoin = [0x20; 4]; and matched as match x {
Foo => do_thing(),
[1, 2, 3, 4] => do_other_thing(),
_ => panic!("Whoopsies!")
} |
@eternaleye That sort of thing gets pretty ugly. Suppose you have
Now what happens if you want to call |
I can see a few potentially-sensible approaches. Firstly, in your example, the caller constructed a More interesting, perhaps, might be this: fn bar<T>(x: T) -> Foo<T> {
X
} Here, the disjoin construction occurs in a context where unification is unknown. I see two-and-a-half approaches here:
Personally, I prefer (1) - it maintains the idea I used for your example that it's the responsibility of the code specifying the type parameter to avoid creating invalid disjoins. |
However if something like #1582 were to get merged then it would make sense to reconsider this RFC. With access to variadic tuples people will end up needing the dual types to variadic tuples aswell. Some examples: Suppose there's a
Another example; suppose there's a
|
@canndrew I guess, analogously to #1582, if we wanted to be able to do induction on these and allow taking references to the "tail" of a disjoin (that is, where the first variant has been eliminated), we'd have to fix the representation so that the discriminant is always a full word, and the alignment is always the same...? |
@flying-sheep nope, I don't have write access to the repo. @glaebhoerl True, the discriminant would have to have a fixed size. But I don't see why alignment would have to be the same. A |
ah, i forgot people can only reopen stuff they closed themselves. so @nikomatsakis: if the team thinks so, could one of you do it, please? 😄 |
@canndrew But if you have a |
I can't see where it would matter though. If you have a |
I was thinking all references to disjoins (and, therefore, their "tail-disjoins") would be to the beginning, including the discriminant, and the difference between the different types would just be in the set of values the discriminant may have. So if you have an Though now I'm confused because what we've just proved, when eliminating the first variant, is that it doesn't contain a And I guess you could special case just |
The way I'm imagining it, the For the alignment of the whole structure, adding a head type Also, yeah I think it would make sense to special-case the representation of single element disjoins. |
Also, in that comment I was assuming that the discriminant was 32bit. On platforms with a 64bit discriminant it might make sense to represent a |
I'd really like to get that generic tuples RFC fixed up, discussed, implemented and merged before looking at this again (because the two RFCs complement each other so well). |
Fwiw another potential type-syntax is (On the other hand, the angle brackets version resembles generics, which is good, because those are type parameters, and bad, because we don't have variadic generics. In addition, the same things are also true of The main problem with this idea is that we'd also need a value-level syntax to go along with it (the |
Rendered view
Edit 2: I have split off a separate RFC for the
!
type here. This RFC now assumes that RFC as a prerequisite.Edit: A lot of people are getting stuck on this point so it bears further clarification.
()
and!
are not the same type. They are related, but different.()
is the type with one value,!
is the type with _zero_ values. What this means:()
can only return()
. This why you don't need a return statement in such a function, because there's only one thing it can possibly return. A function with return type!
can never return at all - because there's nothing that it could return.()
can only be assigned one value (ie. the value()
). A variable of type!
can never be assigned a value at all. This doesn't mean you can't use them, for example it's totally okay to writelet x: ! = panic!()
becausepanic!()
never returns. You could also write a function that takes a!
argument - but that function could never be called.!
can never exist. For example, a value of typeResult<T, !>
is guaranteed to beOk
because there's nothing theErr
variant could hold. Contrast this withResult<T, ()>
which has a valueErr(())
.!
never executes. This is enforced at compile time by the type-checker. Rust doesn't allow uninitialized variables and it's impossible to write (safe) code that initializes a!
.!
can be mapped to any other type. To see why this is so consider the definition of a (pure) function: A function assigns to every value in it's domain a value in it's codomain. For example, to specify a functionfn foo(x: bool) -> i32
we need to specify the values offoo(true)
andfoo(false)
. For this we can use something likematch x { true => 23, false => 34}
. Likewise, to specify a functionfn bar(x: !) -> i32
we need to specify a value for each possiblex
- of which there are none. We could write the function body asmatch x {}
. This works because there is one match branch for each possiblex
and every branch returns ani32
. If you find yourself asking "but what value does that return?", ask yourself "for what x?" and then look at the match statement again. Also, remember that that match statement can never execute.!
can be thought of as a subtype of every other type. Every!
is also aString
, and ani32
, and achar
etc. This is true for the same reason thatIterator::all
returnstrue
on an empty iterator - there are no counterexamples. By contrast, the same is not true of()
. The value()
is not aString
, or ani32
, or achar
etc. This RFC proposed to allow!
to unify with any other type (as it already does) meaning that a variable of type!
can be treated as any type.()
can be thought of as being an empty value (the value()
has no bits of information).!
is an empty type (the type!
has not inhabitants). They both mean "nothing", but it's a different "nothing".Note that
!
is not new or in any way special. We already have all these behaviours with empty enums. Givenenum Empty {}
ande: Empty
we know:Empty
diverges.Empty
can never be callede
can never be assigned a valuee
will never execute.Result<T, Empty>
can only beOk
, neverErr
e
can be mapped into any type withmatch e {}