-
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
Named existentials and impl Trait variable declarations #2071
Conversation
Thanks so much, @cramertj, for taking this on! I wanted to follow up on two points that @nikomatsakis, @eddyb, @cramertj and I have been discussing but didn't make it in full detail in the RFC. Going fully expressiveFirst, the question of pursuing this feature as a next step, rather than adding direction support for I do want to emphasize that I personally am uncomfortable with the situation where you can use Implementation concernsThe RFC strives to stay pretty high-level in terms of the specification, but it has some pretty significant implementation impact. In particular, the fact that a single The idea we've discussed involves doing type inference as usual for each function, while treating instances of an There are a number of options for how to proceed, falling on a spectrum. Here are the two extremes:
The RFC is deliberately leaving the precise resolution of these questions up in the air, since they are best resolved through implementation and experimentation. I personally think we should start with the most conservative approach and go from there. |
text/0000-impl-trait-type-alias.md
Outdated
// Type `Foo` refers to a type that implements the `Debug` trait. | ||
// The concrete type to which `Foo` refers is inferred from this module, | ||
// and this concrete type is hidden from outer modules (but not submodules). | ||
pub type Foo: impl Debug; |
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.
Is this :
meant to be a =
?
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.
Yes-- same for the other one. Thanks for catching that. I'll fix it as soon as I get to a computer.
text/0000-impl-trait-type-alias.md
Outdated
inner: T | ||
}; | ||
|
||
type Foo<T> -> impl Debug; |
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.
Similarly, is this ->
meant to be a =
?
Can I cast (or type ascribe) let displayable = "Hello, world!" as impl Display; Can an impl trait alias be self-referential? type Foo = impl Add<Foo, Output=Foo>; The |
In the current implementation, at least, yes, as the trait bounds are associated to, but distinct from, the type itself, i.e. it's something like this in the compiler: type_of(Foo) = Anon0;
predicates_of(Anon0) = [Anon0: Add, <Anon0 as Add>::Output == Anon0]; |
This RFC wouldn't allow either of those. It's not totally obvious to me what either |
Yes, trait MyTrait {}
type Foo<T> = impl MyTrait;
struct MyStruct<A, B, C> {
a: A,
b: B,
c: C,
}
impl<A, B, C> MyTrait for MyStruct<A, B, C> {}
fn foo<T>(t: T) -> Foo<T> {
// This tells the compiler that `for<T> Foo<T> == MyStruct<i32, T, &'static str>`
MyStruct { a: 1i32, b: t, c: "" }
} |
My way-more-than-two cents on this RFC: I am strongly in favor of the functionality being proposed here.This makes it a lot easier to properly distinguish public APIs from implementation details in a way the compiler reliably enforces, makes it far more feasible to work with otherwise unnameable types like closures or messy "I really don't care what's in here" types like iterator combinators, and the syntax seems about as concise, obvious and ergonomic to me as it could possibly get. This seems very relevant to the "publicly unnameable types" issue, but the RFC never mentions that.I have no idea how widely known that issue is, or what everyone else is calling it these days, so I should probably explain what I mean by it: First, backstory: The only objection I'm aware of to having impl Trait be in the language at all is that it makes unnameable types more common. In particular, it causes types which were nameable within a function or module to become unnameable outside that function or module. I'm calling those "publicly unnameable types" to distinguish them from "everywhere unnameable types" like closures (which this RFC does mention). Note that being able to name a type does not mean being able to rely on that type never changing. For instance, if your library has a function returning i32, and my code puts that i32 in one of my structs, today I need to be able to write "i32" as part of my struct's type definition. If you change that i32 to impl Debug, then I can no longer rely on you always returning i32 anymore (which IS a good thing), but I also can't put your i32 in one of my structs anymore because Rust doesn't provide a way to say "whatever type that function returns" (which is NOT a good thing). Previously, I thought the only solution to this would be adding a typeof operator. Then I could write However, this RFC also alludes to a "sugar" for "impl Trait in traits", though it's never stated exactly what that is since it's not part of this proposal. I assume this hypothetical feature would mean making code like this:
be sugar for this:
If this sugar is added, then using it makes the type unnameable again. So there's an argument we should never actually add this sugar unless we also add something like typeof, or unless there's some reason why trait authors would need the ability to forbid clients from storing their return types in structs (is there one? I'm not aware of one). Now what I actually wanted to say: I think the RFC should address the publicly unnameable types issue. It should at least be an unresolved question, but if the author(s?) actually intended for this to make a typeof operator unnecessary, or less necessary, that should be made explicit. I have no strong opinion on whether we should in the long run add a typeof operator or rely on the proposed impl Trait type aliases or simply reject the idea that all returned types should be nameable in client code, but we shouldn't be committing to or ruling our any of those options by accident. Readability of impl Trait type aliasesIf we ignore interactions with other features for a moment, the only concern I have with this RFC in isolation is that it won't always be obvious what the concrete type of an associated impl type alias is intended to be. I'm fine with making the compiler do a limited form of module-level type inference (assuming the compiler team is confident it won't cause any problems), but there's a risk of also requiring every human who reads the code to do module-level type inference in their heads. If I were to start using this feature as currently proposed, I'd probably add the intended concrete type in a comment every time I wrote such an alias.
It's hard to come up with a counter-proposal though. Adding the concrete type directly to the type alias statement feels bad because it breaks the principle that the signatures of a module's public items contain exactly what client code needs to know and no more (that is a thing in Rust, right? I'm not making that up?), and as cramertj explained "casting" or "ascribing" syntaxes would be pretty confusing here, but if we put the concrete type anywhere else that's not much of an improvement on requiring it to be explicit in every method's return values. So at the moment, I think aturon's suggestion that the conservative implementation would be "each function using the alias, by itself, must contain enough information to fully infer the concrete type for the alias" seems like the best solution to this concern, since the human would probably only need to look at the first method after the associated impl type alias to figure out what the concrete type is. Consider this a vote in favor of "we should start with the most conservative approach and go from there". |
What about full |
@Ericson2314 I'm not sure I understand your proposal. With Is your goal just to use a different syntax? |
The So, I think that this should have another syntax, or at least this should be noted in the "drawbacks" section. |
@dlight That interpretation is incorrect though. It's not a special syntax. Also, syntactical substitution is not guaranteed in Rust, and semantically, an The RFC probably needs more examples such as The only reason to write it as |
In my mental model a type alias is just syntax sugar (not an associated type; just a top-level alias). Replacing aliases by their definition should never change whether a program typechecks. Could you point out some stable Rust code where my intuition is incorrect? Anyway, even if it's incorrect, people may still be misled by it. |
@dlight Path resolution is the most obvious one, since it's done in the scope of the definition. type Bar = [(); {
// Any top level item, including nested modules.
mod foo {
// There is exactly one instance of this module.
pub struct Foo;
impl Foo {
pub const X: usize = 123;
}
}
foo::Foo::X
}]; A more meaningful example, although not available on stable yet: type Baz = [u8; { struct Quux(u8, u16); std::mem::size_of::<Quux>() }]; If we had randomized field reordering, that one one would be guaranteed to always be the same type (i.e. its length would always be evaluated for the same Of course these would make more sense with const generics, with the Alright, so I don't have a perfect example. Still, a There's another angle, I suppose - we can show that type Double<T> = (T, T); Lifetime elision behaves independently of the expansion of fn elision_alias((x, _): Double<&str>) -> &str { x }
// error: "this function's return type contains a borrowed value, but the signature
// does not say which one of `(x, _)`'s 2 lifetimes it is borrowed from"
fn elision_syntax((x, _): (&str, &str)) -> &str { x }
fn impl_trait_alias() -> Double<impl ToString> {
(String::new(), Default::default())
}
// error: "type annotations needed"
fn impl_trait_syntax() -> (impl ToString, impl ToString) {
// ^^^^^^^^^^^^^ cannot infer type for `_`
(String::new(), Default::default())
} |
Thanks. Since type aliases aren't just syntactical already, it makes less sense to add different syntax just for being able to name a I'm unable to find some kind of documentation for this subtle semantics around type aliases and their expansion. I looked on the first book, the second book, and the reference, but perhaps I should look at more advanced stuff (which I'm not sure exists yet?). |
The issue is that there is no formal specification, otherwise I could link to that. There is a similar situation with Really, only macros should involve syntactic expansion and that's why they are invoked with a bang, i.e. |
Perhaps this RFC should mention this sketch of explicit existentials from RFC 1951 in the "Alternatives" section. I'm not sure if its semantics is a subset of this RFC (it looks like it is). |
|
My motivation for making
I think that it would be confusing to new users if we allow WRT type alias syntax: there have been a lot of ideas floated in both this thread and others, so i'll try to briefly outline what I see as some of the main advantages and disadvantages of each proposed syntax:
I also gave some consideration to Overall, my preference is towards
This seems to me like an unnecessary complication. It's relatively easy for the compiler to determine if a function contains enough information to infer the concrete type, so I'd prefer to save users the extra work. |
FWIW eliding the type of a IMO that is a much better approach than struct MyAlloc {...}
pub static MY_ALLOC: impl Allocator = MyAlloc {...}; |
Would I be allowed to do things like: type Foo = (impl Bar, impl Baz); or type IterDisplay = impl Iterator<Item=impl Display>; ? If yes, that's a significant difference relative to the other two syntaxes, where you'd have to introduce a separate |
@glaebhoerl Those are allowed and I've previously mentioned the RFC should be more explicit on it. fn parse_csv<'a>(s: &'a str) -> impl Iterator<Item = impl Iterator<Item = &'a str>> {
s.split('\n').map(|line| line.split(','))
} |
What makes |
Hm, it doesn't even work with
So what's the right way to do this? :) |
Can you please give a concrete example to show this is not good? IMO removing the ability to name the type and making constraint on it is not a bad thing. It clearly tells the code reader: "Here is a type, you should only use its |
@Boscop Huh, I would think that would work, because (also, shouldn't this be posted on the tracking issue?) |
* `async` from rust-lang/rfcs#2394; * `existential` from rust-lang/rfcs#2071.
* Add new keywords * `async` from rust-lang/rfcs#2394; * `existential` from rust-lang/rfcs#2071. * Make `existential` a contextual keyword Thanks @dlrobertson who let me use his PR #284!
This PR contains the building blocks for async p2p services. It consists of the following modules: * builder: contains the MakeServicePair trait which should be implemented by a service builder and the StackBuilder struct which is responsible for building the service and making service handles available to all the other services. Handles are any object which is able to control a service in some way. Most commonly the handle will be a transport::Requester<MyServiceRequest>. * handles: struct for collecting named handles for services. The StackBuilder uses this to make all handles available to services. * transport: This allows messages to be reliably send/received to/from services. A Requester/Responder pair is created using the transport::channel function which takes an impl of tower_service::Service as it's first parameter. A Requester implements tower_service::Service and is used to send requests which return a Future which resolves to a response. The Requester uses a oneshot channel allow responses to be sent back. A Responder receives a (request, oneshot::Sender) tuple, calls the given tower service with that request and sends the result on the oneshot::Sender. The Responder handles many requests simultaneously. Notes: This PR adds the rust feature #![feature(existential_type)] to reduce the need to box futures in many cases - more info here: rust-lang/rfcs#2071 TODO: Hook up pub/sub messages from the comms layer. (Ref #644)
… r=nikomatsakis Fix coherence checking for impl trait in type aliases **UPDATE**: This PR now treats all opaque types as remote. The original description appears below, but is no longer accurate. Fixes rust-lang#63677 [RFC 2071](rust-lang/rfcs#2071) (impl-trait-existential-types) does not explicitly state how `type_alias_impl_trait` should interact with coherence. However, there's only one choice which makes sense - coherence should look at the underlying type (i.e. the *"defining"* type of the `impl Trait`) of the type alias, just like we do for non-`impl Trait` type aliases. Specifically, `impl Trait` type aliases that resolve to a local type should be treated like a local type with respect to coherence (e.g. `impl Trait` type aliases which resolve to a foreign type should be treated as a foreign type, and those that resolve to a local type should be treated as a local type). Since neither inherent impls nor direct trait impl (i.e. `impl MyType` or `impl MyTrait for MyType`) are allowed for type aliases, this usually does not come up. Before we ever attempt to do coherence checking, we will have errored out if an `impl Trait` type alias was used directly in an `impl` clause. However, during trait selection, we sometimes need to prove bounds like `T: Sized` for some type `T`. If `T` is an impl trait type alias, this requires to know the coherence behavior for `impl Trait` type aliases when we perform coherence checking. Note: Since determining the underlying type of an `impl Trait` type alias requires us to perform body type checking, this commit causes us to type check some bodies easier than we otherwise would have. However, since this is done through a query, this shouldn't cause any problems For completeness, I've added an additional test of the coherence-related behavior of `impl Trait` type aliases. cc rust-lang#63063
Add the ability to create named existential types and support impl Trait in let, const, and static declarations.
Rendered