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 upGeneric associated types (associated type constructors) #1598
Conversation
kennytm
reviewed
Apr 30, 2016
| } | ||
| ``` | ||
|
|
||
| `not has the type `bool -> bool` (my apologies for using a syntax different |
This comment has been minimized.
This comment has been minimized.
P1start
reviewed
Apr 30, 2016
| ``` | ||
|
|
||
| `&[u8]` is not an elided form of some `&'a [u8]`, but a type operator of the | ||
| kind `& -> *`. |
This comment has been minimized.
This comment has been minimized.
P1start
Apr 30, 2016
Contributor
This syntax feels a bit too magic to me—&[u8] almost always acts as &'a [u8] with 'a elided, and it’s not immediately obvious at a glance that Item is actually a higher-order type of kind & -> *. Also, what if multiple lifetime parameters are omitted? Is &&T equivalent to <'a, 'b> => &'a &'b T or <'a, 'b> => &'b &'a T?
I would prefer a more obvious (but more verbose) syntax: StreamingIterator<Item<'a>=&'a [u8]>. That way it’s apparent that 'a is declared as part of the trait bound, and it also nicely matches the syntax for associated item definitions.
This comment has been minimized.
This comment has been minimized.
glaebhoerl
Apr 30, 2016
•
Contributor
That also works better if the associated type is generic over a type rather than a lifetime -- Foo<Bar<T>=&'static [T]> is certainly more obvious than Foo<Bar=&'static []>, for example.
This comment has been minimized.
This comment has been minimized.
arielb1
Apr 30, 2016
•
Contributor
It certainly fits the pattern of lifetime elision done today - fn(&[u8]) is today for<'a> fn(&'a [u8]), so Stream<Item=&[u8]> is Stream<Item=λ'a. &'a [u8]>. Of course we probably want a fully-explicit way of writing these things.
By the way, I would not like to use & to refer to lifetime - it has a perfectly good meaning of reference today.
This comment has been minimized.
This comment has been minimized.
eddyb
Apr 30, 2016
Member
By the way, I would not like to use & to refer to lifetime - it has a perfectly good meaning of reference today.
I agree, why not '*?
This comment has been minimized.
This comment has been minimized.
krdln
Apr 30, 2016
Contributor
The "elision" syntax (I mean StreamingIterator<Item=&[u8]>) makes sense for '* → * (& → *) types and I don't think it would create confusion or ambiguity. But for bigger number of input params or non-lifetime inputs I can't image how it would work, so we need a non-elided syntax too. I thought of reusing the closure syntax (Item = |'a| &'a[u8]), but the one proposed by @P1start (Item<'a>=&'a [u8]) looks more Rusty imho.
@withoutboats You are talking about allowing any operators, but the syntax seems only defined for constructors. If you plan to support any operators, could you give an example of using * → * → * associated operator (not (*, *) → *)? Also, an example for type as an input would be nice too. I know it's analogous to lifetime as an input, but it would be nice to have it for completeness. An example would be also nice for multi-input constructor.
This comment has been minimized.
This comment has been minimized.
eddyb
Apr 30, 2016
Member
@krdln The "type-level closure syntax" already exists, and the example would be Item = for<'a> &'a [u8], although I prefer Item<'a> = &'a [u8] myself.
This comment has been minimized.
This comment has been minimized.
krdln
Apr 30, 2016
•
Contributor
@eddyb The . The (implements) for<'a> 'a[u8] means ∀'a (implements) &'a[u8](implements) for<'a> Trait<'a> means ∀'a (implements) Trait<'a>. I think it's kind-of analogous, but a totally different thing than a type-lambda. And it would be confusing to use the same syntax for those two things.
If you squint, you can imagine that for<'a> Trait<'a> has a type signature of (& → * → bool) → (* → bool). Which means "take a lifetime parametrised (& → ...) trait (* → bool) and return a trait (* → bool)"* → bool (a trait bound). And that's a totally different thing than & → *.
This comment has been minimized.
This comment has been minimized.
eddyb
Apr 30, 2016
Member
T: for<'a> Trait<'a> is sugar for for<'a> T: Trait<'a>, i.e. the universal quantification covers the whole bound.
I suppose it would be a bit weird if the following two snippets were equivalent:
type Foo = for<'a> fn(&'a T) -> &'a U;
type Bar = for<'a> Trait<'a>;type Foo<'a> = fn(&'a T) -> &'a U;
type Bar<'a> = Trait<'a>;And if omitting explicit <'x> would mean universal quantification, which conflicts with anonymous lifetimes in a function signature and/or elision.
Although, this is only a problem with lifetimes AFAICT, unless there are situations where you can actually have a Box<for<T: Foo> Trait<T>>.
This comment has been minimized.
This comment has been minimized.
withoutboats
Apr 30, 2016
Author
Contributor
I would prefer a more obvious (but more verbose) syntax: StreamingIterator<Item<'a>=&'a [u8]>. That way it’s apparent that 'a is declared as part of the trait bound, and it also nicely matches the syntax for associated item definitions.
I agree that this syntax is quite magic (and is easily the least satisfying syntax here to me), but I'm concerned about how much this looks like some sort of higher-rank syntax. If we do add an ability to make these parameters higher-rank, I am a bit concerned about the difference in meaning about 'a between Item<'a>=&'a [u8] and for<'a> Item<'a>: Display.
But for bigger number of input params or non-lifetime inputs I can't image how it would work, so we need a non-elided syntax too.
You're right, the elision syntax is right out for that reason.
By the way, I would not like to use & to refer to lifetime - it has a perfectly good meaning of reference today.
This syntax is not internal to Rust of course. I find '* hard to distinguish from *. I think the best syntax might be the clearest, using words: lifetime -> type.
This comment has been minimized.
This comment has been minimized.
glaebhoerl
Apr 30, 2016
Contributor
I think the best syntax might be the clearest, using words:
lifetime -> type.
I agree!
Not even Haskellers really like their * syntax for "type" and in GHC 8 they're getting Type as an alias for it.
This comment has been minimized.
This comment has been minimized.
|
I agree that this seems like the ideal starting point for adding higher-kinded capability to Rust. Even the idea of moving forward incrementally in this way, as opposed to "Adding Higher-Kinded Types" all at once in a "big bang", is something that hadn't occurred to me, but seems obvious in hindsight. To stick with our own terminology consistently, instead having of a mishmash of vocabulary from Rust, Haskell, and the academic literature, I would probably prefer to call this feature "generic associated types" or "associated generic types". (I would also prefer to call "higher-kinded types" instead "higher-order generic types", contrasting with "higher-rank types" which are "higher-order generic functions", but the usage of "HKT" is incredibly widespread by this point.) I also suspect that most of the difficult design work which needs to be done before this feature can move forward is on how to extend the type system to support it, which this RFC (self-admittedly) doesn't even attempt to address. Am I right? |
This comment has been minimized.
This comment has been minimized.
dwrensha
commented
Apr 30, 2016
I'm interested in seeing a concrete example of something that cannot be expressed using this hack. I've hit situations where I've wanted associated type operators as proposed in this RFC, but in every case it has sufficed hoist the the type parameters up to the trait itself. See, for example, these |
This comment has been minimized.
This comment has been minimized.
Ideally, the |
This comment has been minimized.
This comment has been minimized.
|
AFAIK the bulk of the implementation work needed here is removing the constraints on the "3 application levels" in the current compiler (type/ The actual resolution of non-monomorphic associated types won't actually be that big of a problem, we already have to do that sort of transformation for generic methods. And lastly, what I'm worried most about is checking uses of such a trait with polymorphic associated types without providing concrete bindings. But overall, I like this RFC and @rust-lang/compiler will continue to work towards making such advances possible, regardless of which exact syntactic or semantic choices we end up accepting. |
This comment has been minimized.
This comment has been minimized.
|
@dwrensha unsafe trait Ref {
type Item<'a>;
unsafe fn erase_lifetime<'a>(x: Item<'a>) -> Item<'static> { transmute(x) }
unsafe fn introduce_lifetime<'a>(x: Item<'static>) -> Item<'a> { transmute(x) }
}The main benefit from this RFC here would be that compiler would know that all types created by the same It seems that the main benefit from this RFC comes from allowing lifetime ( |
This comment has been minimized.
This comment has been minimized.
|
@krdln Actually, right now you can't even use |
This comment has been minimized.
This comment has been minimized.
|
@eddyb Your example doesn't compile, since you can't apply an lifetime paremeter to Using your suggestion, I came up with this, so this use case might be supported in current Rust, with some fixes for transmute behaviour. |
This comment has been minimized.
This comment has been minimized.
|
@krdln Hah, that was dumb of me. Totally missed the fact that you were applying multiple times within the |
This comment has been minimized.
This comment has been minimized.
soltanmm
commented
Apr 30, 2016
•
|
@krdln I think there does exist an injection between what is proposed here and the syntax that exists today, but it requires a few more pieces than what @dwrensha pointed out. use std::mem;
// '* -> *
trait R2T<'a> {
type T;
}
// * -> *
trait T2T<A> {
type T;
}
// ('* -> *)('*)
type AppR<'a, T> where T: R2T<'a> = <T as R2T<'a>>::T;
// (* -> *)(*)
type AppT<A, T> where T: T2T<A> = <T as T2T<A>>::T;
// A trait with a higher kinded operator
unsafe trait Ref {
type Item: for<'a> R2T<'a>;
unsafe fn erase_lifetime<'a>(x: AppR<'a, Self::Item>) -> AppR<'static, Self::Item> { mem::transmute(x) }
unsafe fn introduce_lifetime<'a>(x: AppR<'static, Self::Item>) -> AppR<'a, Self::Item> { mem::transmute(x) }
}
// A type with a '* -> '* -> * HKT
trait MyType
where
for<'a> Self::BiRefItem: R2T<'a>,
for<'a, 'b> AppR<'a, Self::BiRefItem>: R2T<'b>
{
type BiRefItem;
}
// Use of partial application by currying (which can always be extended to full partial application with helpers)
trait PartiallyApplied<'a, T: MyType>
where
Self::Ref: Ref<Item=AppR<'a, T::BiRefItem>>
{
type Ref;
// ...
}
fn main() {}However, it is excessively verbose, highly error prone, works only for lifetimes in certain use-cases (because it depends on HRTBs), and there're some issues with the compiler that prevent it from compiling (see rust-lang/rust#30472 and rust-lang/rust#31580). For a more practical application of the general approach that does work today, see #1403 (comment). For background on the general approach, see the fine-print in the associated items RFC. n.b. the RFC uses non-existent syntax in its example. and I am in no way suggesting that one take a syntax-sugar approach implementing this :-P |
This comment has been minimized.
This comment has been minimized.
soltanmm
commented
Apr 30, 2016
•
|
Actually, now that I think about it... Unless this RFC is limited to kinds that look like |
This comment has been minimized.
This comment has been minimized.
|
@soltanmm It is limited to |
This comment has been minimized.
This comment has been minimized.
I don't feel strongly about the terminology, but I'll note that I avoided the term "higher-kinded type" in this RFC because I found it very confusing before I read TaPL, which avoids that terminology. A big part of my confusion was that calling type operators "types" suggests to me that this is a language feature that allows one to create values of a "type" of a higher kinded, like I also don't think we use the term generics that much, do we? I usually see 'type parameter' or 'parameterized type.'
I think others have answered this well (the most obvious thing is:
I used the term operator throughout the text, but its correct that this would only allow associated type constructors.
I don't know anything about how HRTBs are implemented, some of eddyb's comments suggest this might be an easy extension, but the higher-ranked parameter is being applied in a different position in these two expressions: for<'a> Type<'a>: Trait
Type: for<'a> Trait<'a>This RFC explicitly says it doesn't propose allowing Also, this RFC only discusses Extending HRTB to be used with associated items of a kind other than |
This comment has been minimized.
This comment has been minimized.
|
Thinking about the syntactic and semantic complexities of bounds using associated type constructors, and the fact that all of this seems to be conceivable as just an extension of HRTBs, I think it might be a good idea to not even discuss bounds using associated type constructors in this RFC, and instead delegate that to an RFC about extending HRTBs. |
This comment has been minimized.
This comment has been minimized.
|
@withoutboats Both of those forms are allowed right now and the In a way, So
It's not a good idea to punt on this matter, because the |
This comment has been minimized.
This comment has been minimized.
I'm not sure why this makes it bad to punt, but I think the important takeaway is that we (yet) don't need to be able to put type constructors in where clauses, because we can use HRTB. Obviously this won't be sufficient for later use cases involving higher-kinded traits. |
This comment has been minimized.
This comment has been minimized.
|
Another issue has occurred to me just now. We would probably also want some sort of implicit HRTBs at the declaration site of the associated type, as in: trait Collection {
type Elem;
type Iter<'a>: Iterator<Item=&'a Self::Elem>;
type IterMut<'a>: Iterator<Item=&'a mut Self::Elem>;
...
} |
This comment has been minimized.
This comment has been minimized.
|
@withoutboats I wonder if we should agree on using "polymorphic" or some other "painfully clear" term because I thought you meant |
This comment has been minimized.
This comment has been minimized.
|
@eddyb: Right, the RFC currently discusses |
This comment has been minimized.
This comment has been minimized.
|
I've edited and expanded the RFC in response to the comments so far, mainly clarifications and adding content about how this would require HRTB changes. |
withoutboats
force-pushed the
withoutboats:associated_hkts
branch
2 times, most recently
from
188eaeb
to
c73b60e
May 2, 2016
nrc
added
the
T-lang
label
May 2, 2016
withoutboats
changed the title
Associated type operators (a form of higher-kinded polymorphism).
Associated type constructors (a form of higher-kinded polymorphism).
May 2, 2016
soltanmm
reviewed
May 2, 2016
| 1. First, allowing HRTBs to applied to type constructors on the left-hand side | ||
| of the bound. Currently they are only available to traits on the right-hand | ||
| side of the bound. | ||
| 2. Second, allowing HRTBs to introduce type parameters, instead of only |
This comment has been minimized.
This comment has been minimized.
soltanmm
May 2, 2016
When's the RFC to spec out the syntax of bounds/where-clauses on HRTB-quantified types coming out?
aaand obligatory cc #1481
soltanmm
reviewed
May 2, 2016
| Enabling this requires extending HRTBs in two different ways: | ||
|
|
||
| 1. First, allowing HRTBs to applied to type constructors on the left-hand side | ||
| of the bound. Currently they are only available to traits on the right-hand |
This comment has been minimized.
This comment has been minimized.
soltanmm
May 2, 2016
okay github hopefully you won't just eat my comment this time...
I thought we determined otherwise. Or do you mean something outside the form of for<'x> T: U? If something different it's probably worth clarifying.
This comment has been minimized.
This comment has been minimized.
withoutboats
May 2, 2016
Author
Contributor
You're right, I misunderstood and thought that it was disallowed syntactically.
This comment has been minimized.
This comment has been minimized.
|
The syntax trait StreamingIterator {
type Item;
}
struct Foo<T> where T: for<'a> StreamingIterator<Item=&'a mut [u8]> {
x: T
}Granted, as far as I can tell there is no way to write T which satisfies this constraint at the moment, but as far as I can tell that's orthogonal to this RFC. I think what you actually want is something closer to |
This comment has been minimized.
This comment has been minimized.
|
You're right that its wrong, but it needs both the EDIT: Fixed. |
withoutboats
force-pushed the
withoutboats:associated_hkts
branch
from
c73b60e
to
8e922c0
May 2, 2016
burdges
referenced this pull request
Sep 1, 2017
Closed
Rand crate revision (pre-stabilisation) #2106
This comment has been minimized.
This comment has been minimized.
|
Merging this finally :) |
This comment has been minimized.
This comment has been minimized.
ashleysommer
commented
Sep 2, 2017
This comment has been minimized.
This comment has been minimized.
|
R u tho? ;) |
This comment has been minimized.
This comment has been minimized.
|
Talked to Niko right after I posted that and found out that the thing I thought was an unresolved question was a resolved question and I have to write the section of the RFC for it, but yes |
withoutboats
merged commit a7cd910
into
rust-lang:master
Sep 2, 2017
This comment has been minimized.
This comment has been minimized.
|
Done :) |
withoutboats
referenced this pull request
Sep 2, 2017
Open
🔬 Tracking issue for generic associated types (GAT) #44265
This comment has been minimized.
This comment has been minimized.
dylanede
commented
Sep 8, 2017
|
Just tried exploring how
Would mean that the following is not possible (note the equality bound)? trait FunctorFamily {
type Functor<T>;
fn fmap<F, A, B>(x: Self::Functor<A>, f: F) -> Self::Functor<B> where F : Fn(A) -> B;
}
// The following trait is for usability
trait Functor<T> {
type Family: FunctorFamily<Functor<T>=Self>; // equality bound
fn fmap<F, B>(self, f: F) -> Self::Family::Functor<B> where F : Fn(Self) -> B;
}
impl<T, A> Functor<A> for T::Functor<A> where T : FunctorFamily {
type Family = T;
fn fmap<F, B>(self, f: F) -> T::Functor<B> where F : Fn(A) -> B {
T::fmap(self, f)
}
}Is there an alternative way of phrasing |
golddranks
referenced this pull request
Sep 11, 2017
Closed
Recently merged GAT RFC has some relevance to the API design #88
jgouly
added a commit
to jgouly/keyboard-app
that referenced
this pull request
Oct 6, 2017
This comment has been minimized.
This comment has been minimized.
ExpHP
commented
Oct 17, 2017
This comment has been minimized.
This comment has been minimized.
Boscop
commented
Nov 3, 2017
|
Is there already a plan how HKTs will look outside of traits, so that there is still "design space" to make the syntax the same as in traits? How would it look in functions, like this? fn foo<M<type, const usize>, T, const N: usize>() -> M<T, N>;
fn foo<M<type, const usize>, T, const N: usize>()
where for<T, N> M<T, N>: Default
-> M<T, N> {
Default::default()
} |
This comment has been minimized.
This comment has been minimized.
gbutler69
commented
Mar 11, 2018
•
|
It would be nice if any RFC that uses an acronym provides a link or definition upon first use. HRTB for example. So, this:
Would become this:
|
This comment has been minimized.
This comment has been minimized.
dylanede
commented
Apr 16, 2018
|
I haven't seen any mention of variance with respect to GATs yet. The RFC should note whether associated types are invariant, covariant or contravariant in their type parameters. My experiments with poor-man's GATs ( |
This comment has been minimized.
This comment has been minimized.
Boscop
commented
Apr 21, 2018
•
How about this? That's a real-world example I'm running into:
Does anyone of you have any idea how I can make this work? :) I need to be able to instantiate/store Or is there any way I can express/enforce that AFAIK, to do this I'd either need the ability to express the above statement (and I'd need rustc to then allow me to use I tried to do it with GATs like this: impl DeviceApp for MyAppState {
type BorrowedState<'a> = MyBorrowedState<'a>;but I run into this ICE:
I really need this to work. What would be the recommended way to do this before this ICE issue gets fixed? :) Btw, how long do you think it will take until this issue gets fixed? |
This comment has been minimized.
This comment has been minimized.
Boscop
commented
Apr 21, 2018
•
|
I found a workaround, pulling https://play.rust-lang.org/?gist=9b7907d94dfc28aabe12f5f90f8c4592&version=stable pub trait DeviceSpecific {
type Dev: Device;
}
pub trait DeviceApp<'a>: DeviceSpecific {
// type Dev: Device;This compiles, but it's a combo of 2 ugly workarounds (I'd prefer not to split the trait like that) that wouldn't be necessary with proper (working) GATs, right? Can you think of any solution that doesn't require me to split the trait like that? :) |
This comment has been minimized.
This comment has been minimized.
Boscop
commented
Apr 21, 2018
•
|
Ah no, it doesn't work with the impl of pub struct AppMgr<T: for<'a> DeviceApp<'a>> {
device: Option<T::Dev>,
check_connection: AtFps,
in_port_name: String,
out_port_name: String,
app: T,
}
impl<T: for<'a> DeviceApp<'a>> AppMgr<T> {
// error: cannot extract an associated type from a higher-ranked trait bound in this context
pub fn process(&mut self, now: u64, v: &mut T::BorrowedState) -> Vec<T::Event> {
// ^^^^^^^^^^^^^^^^
if self.check_connection.is_due(now) {
let should_reconnect = if let Some(ref mut device) = self.device {
!device.is_connected()
} else {
true
};
if should_reconnect {
self.device = T::Dev::connect(&self.in_port_name, &self.out_port_name).ok();
}
}
if let Some(ref mut device) = self.device {
let app = &mut self.app;
device.frame(now, |device, input| { app.process(device, input, v) })
} else {
vec![]
}
}
}What I need is this: // pub struct AppMgr<T: for<'a> DeviceApp<'a>> {
pub struct AppMgr<T: DeviceApp> { // DeviceApp not depending on the lifetime 'a that process() gets called with
// ...
}
// impl<T: for<'a> DeviceApp<'a>> AppMgr<T> {
impl<T: DeviceApp> AppMgr<T> {
// instantiating T::BorrowedState with <'a> from the scope that process() is called in
pub fn process<'a>(&mut self, now: u64, v: &'a mut T::BorrowedState<'a>) -> Vec<T::Event> {
// ...
}
}Is there any way to do this now? If not, will this work with proper working GATs? |
This comment has been minimized.
This comment has been minimized.
Boscop
commented
Apr 21, 2018
•
|
I found a way to impl AppMgr that works: pub struct AppMgr<T: for<'a> DeviceApp<'a>> {
device: Option<T::Dev>,
// ...
}
impl<T: for<'a> DeviceApp<'a>> AppMgr<T> {
pub fn process<'a>(&mut self, now: u64, msgs: Vec<<T as DeviceApp<'a>>::Msg>, v: &'a mut <T as DeviceApp<'a>>::BorrowedState) -> Vec<<T as DeviceApp<'a>>::Event> {
// ...Here as reduced working example: But I still wasn't happy about having to split the trait, so I thought, if I mandate that /*
pub trait DeviceSpecific {
type Dev: Device;
}
pub trait DeviceApp<'a>: DeviceSpecific {
*/
pub trait DeviceApp<'a> {
type Dev: Device + 'static;
// ...
pub struct AppMgr<T: for<'a> DeviceApp<'a>> {
// device: Option<T::Dev>,
device: Option<<T as DeviceApp<'static>>::Dev>, // because Dev: 'static, i thought it will be unifyable with any 'a
app: T,
// ...
}But now I get this error:
https://play.rust-lang.org/?gist=d4084c0a87890d4ed17d0491e453565b&version=stable Why can't rustc unify Actually, for some reason, in my real code I get a different (similar) error instead (with nightly 2018-03-06): error: free region `'a` does not outlive free region `'static`
--> src\devices\mod.rs:132:15
|
132 | if let Some(ref mut device) = self.device {
| ^^^^^^^^^^^^^^Any idea how to make it work with |
Boscop
referenced this pull request
Apr 22, 2018
Open
Rustc should be able to unify `<T as A<'static>>::B>` with `<T as A<'a>>::B>` for all `'a`, given `A::B: 'static` #50166
This comment has been minimized.
This comment has been minimized.
burdges
commented
Aug 6, 2018
|
I quite like the
or
In principle, these could be done with |

withoutboats commentedApr 30, 2016
•
edited by mbrubeck
I definitely realize that this feature may not be prioritized right now, and I expect that some aspects of it probably present real implementation challenges (especially partial application of type operators and extending HRTBs), but I wanted to document and discuss what seems to me like the best incremental step toward higher-kinded polymorphism in Rust.
Rendered
[edited to update rendered link —mbrubeck]