Join GitHub today
GitHub is home to over 31 million developers working together to host and review code, manage projects, and build software together.
Sign upNamed existentials and impl Trait variable declarations #2071
Conversation
aturon
added
the
T-lang
label
Jul 20, 2017
aturon
self-assigned this
Jul 20, 2017
This comment has been minimized.
This comment has been minimized.
|
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. |
Ixrec
reviewed
Jul 20, 2017
| // 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; |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
cramertj
Jul 21, 2017
Author
Member
Yes-- same for the other one. Thanks for catching that. I'll fix it as soon as I get to a computer.
Ixrec
reviewed
Jul 20, 2017
| inner: T | ||
| }; | ||
| type Foo<T> -> impl Debug; |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
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 |
This comment has been minimized.
This comment has been minimized.
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 comment has been minimized.
This comment has been minimized.
This RFC wouldn't allow either of those. It's not totally obvious to me what either |
This comment has been minimized.
This comment has been minimized.
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: "" }
} |
This comment has been minimized.
This comment has been minimized.
|
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". |
tomaka
referenced this pull request
Jul 22, 2017
Closed
Allow explicitly expressing the type of a -> impl Trait #1738
glaebhoerl
referenced this pull request
Jul 25, 2017
Open
Tracking issue for RFC 1861: Extern types #43467
This comment has been minimized.
This comment has been minimized.
|
What about full |
This comment has been minimized.
This comment has been minimized.
|
@Ericson2314 I'm not sure I understand your proposal. With Is your goal just to use a different syntax? |
This comment has been minimized.
This comment has been minimized.
dlight
commented
Jul 26, 2017
|
The So, I think that this should have another syntax, or at least this should be noted in the "drawbacks" section. |
This comment has been minimized.
This comment has been minimized.
|
@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 |
This comment has been minimized.
This comment has been minimized.
dlight
commented
Jul 26, 2017
|
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. |
This comment has been minimized.
This comment has been minimized.
|
@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())
} |
This comment has been minimized.
This comment has been minimized.
dlight
commented
Jul 26, 2017
•
|
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?). |
This comment has been minimized.
This comment has been minimized.
|
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. |
This comment has been minimized.
This comment has been minimized.
dlight
commented
Jul 27, 2017
|
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). |
This comment has been minimized.
This comment has been minimized.
|
This comment has been minimized.
This comment has been minimized.
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. |
This comment has been minimized.
This comment has been minimized.
|
FWIW eliding the type of a IMO that is a much better approach than struct MyAlloc {...}
pub static MY_ALLOC: impl Allocator = MyAlloc {...}; |
This comment has been minimized.
This comment has been minimized.
|
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 |
This comment has been minimized.
This comment has been minimized.
|
@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(','))
} |
This comment has been minimized.
This comment has been minimized.
What makes |
This comment has been minimized.
This comment has been minimized.
|
@alexreg we need an RFC for that. |
This comment has been minimized.
This comment has been minimized.
|
@alexreg If you're planning to write one, you should read mine first and let me know what you think / what you would change. |
This comment has been minimized.
This comment has been minimized.
alexreg
commented
Sep 12, 2018
|
@cramertj Ah cool. Is there a PR for it yet? I'll give you feedback soon. :-) |
This comment has been minimized.
This comment has been minimized.
|
@alexreg No, I wrote it more than a year ago and left it unopened because I think it makes some unfortunate tradeoffs (inferred associated types in particular) in order to get consistently-nameable types. I'm not sure that's something we want to do, and I wanted to get more experience using named existentials in practice before we went down that road. |
This comment has been minimized.
This comment has been minimized.
|
@cramertj read it; looks good overall -- may need some polish textually. Quick notes:
I'm interested in collaborating with you and @alexreg on this if you are game. |
This comment has been minimized.
This comment has been minimized.
Nah, it's not orthogonal at all-- it's the whole feature. it's the only reason you can write |
This comment has been minimized.
This comment has been minimized.
|
The big issue that comes up again and again with RPIT in traits is the ability to name the return type. Associated type inference is the mechanism by which the RFC solves that problem, but it's definitely controversial and unfortunate.
Yeah, note that I wrote this RFC long before the impl-trait-in-arg-position feature was implemented, at which point we didn't consider it part of the RFC. it was something that got approved later via an FCP during the implementation process. |
This comment has been minimized.
This comment has been minimized.
I'm suggesting that initially, you shouldn't be able to wind up with an associated type that you can name and that the only way to name the type returned is via E.g. we initially only allow: trait Foo {
fn bar<...>(...) -> impl Baz; // optional default definition...
}
impl Foo for MyType {
// You are not allowed to specify anything but `-> impl Baz` here.
fn bar<...>(...) -> impl Baz { .. }
}As you noted in the RFC, this is limiting in particular wrt. existing traits and "mixing and matching"; but it is a start. What my main point is that associated type inference and the above is separable as features and can be introduced separately without future-compat issues.
I only agree that it is controversial; I don't necessarily agree it is unfortunate -- in fact it seems like it could be a big boon for ergonomics for
I inferred as much :) |
This comment has been minimized.
This comment has been minimized.
Ah, see, that's the issue-- I don't know if we ever want that, because having the return type of the method be completely unnameable means that there's no way to impose additional bounds on it, something which is needed in many if not most use-cases for |
Centril
referenced this pull request
Sep 13, 2018
Closed
generic existential types should (at most) warn, not error, if a type parameter is unused #54184
This comment has been minimized.
This comment has been minimized.
And by this you mean that you don't know if we ever only want that?
It's not completely unnameable; you can write: type NameIt = impl Baz;
fn stuff(..) -> NameIt {
<MyType as Foo>::bar(..)
}
There's always (#2524): type Foo = _;which affords all operations that the underlying type does. I think there are 4 distinct features here: (F1) trait Alan {
type Turing: Alonzo<Church = impl Kurt>;
// ^ this should be equivalent to writing:
type Turing: Alonzo<Church: Kurt>;
// ^ per RFC 2289
// ^ or equivalently: trait Alan where Self::Turing: Alonzo<Church: Kurt>
const Gödel: impl Haskell;
// ^ Gödel is optionally generic (GATs).
fn curry<'william>(...) -> impl 'william + Howard;
}Using (F2) impl Bob for Atkey {
type Conor = impl McBride; // RFC 2071 + 2515
const PHILIP: impl Wadler = 42;
fn thierry() -> impl Coquand { .. }
}Using (F3) Letting trait implementations to have a type trait Stephanie {}
struct Weirich;
impl Stephanie for Weirich {}
trait Ulf {
fn norell() -> impl Stephanie;
}
struct Emily;
impl Ulf for Emily {
fn norell() -> Weirich { Weirich }
}And now you may assume that (F4) Associated type inference. E.g.: struct Riehl;
impl Iterator for Riehl {
fn next(&mut self) -> Option<EugeniaCheng> { ... }
}The dependency graphs are roughly:
We could thus for example do F1, F2, F3 without doing F4. |
This comment has been minimized.
This comment has been minimized.
|
Further thought: The value of F4 is more clearly shown when used in conjunction F1-F3 but it is useful on its own. Also: there are more places in which
which is appealing if you are in the consistency/uniformity-extermism school of language design ;) |
This comment has been minimized.
This comment has been minimized.
No, I'm unsure about if we ever want to support trait Polygon { ... }
trait Rectangle { ... }
struct Square { ... }
impl Rectangle for Square { ... }
trait IntoPolygon {
fn into_polygon(self) -> impl Polygon;
}
fn do_something_with_rectangleable<T: IntoPolygon>(x: T) where T::???: Rectangle {
let rectangle: impl Rectangle = x.into_polygon();
...
}In my experience, most traits with |
This comment has been minimized.
This comment has been minimized.
Boscop
commented
Sep 16, 2018
•
|
@cramertj But there are trait methods that don't have the needs for bounds like that, just like free functions that only promise to return a trait IntoPolygon {
type Poly = impl Polygon;
fn into_polygon(self) -> Self::Poly;
}Assoc types are defined by each trait Foo {
fn bar() -> impl Iterator<Item = i32>;
}than trait Foo {
type BarReturnType = impl Iterator<Item = i32>;
fn bar() -> Self::BarReturnType;
}if that type is never used in any checks, and not intended to be checked. |
This comment has been minimized.
This comment has been minimized.
|
@Boscop Why not... just... |
This comment has been minimized.
This comment has been minimized.
It's this bit that I think is an antipattern. Needing to name a return type like this or place bounds on one is very common. I would advise everyone, certainly library authors, never to use a feature that made traits with unnameable return types. I don't think we should add features to the language that we then advocate against using. |
This comment has been minimized.
This comment has been minimized.
Boscop
commented
Oct 13, 2018
This comment has been minimized.
This comment has been minimized.
Boscop
commented
Oct 13, 2018
•
|
Hm, it doesn't even work with
So what's the right way to do this? :) |
jonhoo
referenced this pull request
Oct 15, 2018
Open
Incorrect inference of lifetime bound for existential type #55099
This comment has been minimized.
This comment has been minimized.
earthengine
commented
Oct 16, 2018
•
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 |
AltSysrq
referenced this pull request
Oct 27, 2018
Closed
Implementing Arbitrary, what to name Strategy? #98
This comment has been minimized.
This comment has been minimized.
|
@Boscop Huh, I would think that would work, because (also, shouldn't this be posted on the tracking issue?) |

cramertj commentedJul 20, 2017
•
edited by mbrubeck
Add the ability to create named existential types and support impl Trait in let, const, and static declarations.
Rendered