Open aturon opened 9 years ago
One use case I’ve had for this is a default method whose return type is an associated type:
/// "Callbacks" for a push-based parser
trait Sink {
fn handle_foo(&mut self, ...);
// ...
type Output = Self;
fn finish(self) -> Self::Output { self }
}
This means that Output
and finish
need to agree with each other any given impl
. (So they often need to be overridden together.)
default methods should not be able to assume anything about associated types.
This seems very restrictive (and in particular would prevent my use case). What’s the reason for this?
@SimonSapin
This seems very restrictive (and in particular would prevent my use case). What’s the reason for this?
There is a basic tradeoff here, having to do with soundness. Basically, there are two options at the extreme:
This is described in slightly more detail in the RFC.
Originally we proposed to go with the second route, but it has tended to feel pretty hokey, and we've been leaning more toward the first route.
(Incidentally, much the same question comes up for specialization.)
I'm happy to elaborate further, but wanted to jot off a quick response for now.
Would it be possible to track which default methods rely of which associated types being the default, so that only those need to be overridden?
@SimonSapin Possibly, but we have a pretty strong bias toward being explicit in signatures about that sort of thing, rather than trying to infer it from the code. It's always a bad surprise when changing the body of a function in one place can cause a downstream crate to fail typechecking.
You could consider having some explicit "grouping" of items that get to see a given default associated type.
I'm not sure if you've been following the specialization RFC, but part of the proposal is a generalization of default implementations in traits. Imagine something like the following alternative design for Add
(rather than having AddAssign
separately):
trait Add<Rhs=Self> {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
fn add_assign(&mut self, Rhs);
}
// the `default` qualifier here means (1) not all items are impled
// and (2) those that are can be further specialized
default impl<T: Clone, Rhs> Add<Rhs> for T {
fn add_assign(&mut self, rhs: R) {
let tmp = self.clone() + rhs;
*self = tmp;
}
}
This feature lets you refine default implementations given additional type information. You can have many such default impl
blocks for a given trait, and the follow specialization rules.
The fact that you can have multiple such blocks might give us a natural way to delimit the scopes in which associated types are visible (and hence in which the methods have to be overridden when those types are).
(cc @nikomatsakis on that last comment)
@aturon I assume you mean specifically this last paragraph:
The fact that you can have multiple such blocks might give us a natural way to delimit the scopes in which associated types are visible (and hence in which the methods have to be overridden when those types are).
I find this idea appealing, I just think we have to square it away carefully, especially with the precise fn types we ought to be using. :)
Is this feature still being worked on?
What do you think of something like this as a use case for this feature?
trait Bar {
type Foo;
// We want to default to just using Foo as its DetailedVariant
type DetailedFoo = Self::Foo;
// ...other trait methods...
fn detailed(&self) -> DetailedFoo;
// ...other trait methods...
}
@sunjay nobody is working on this, I think we are still unsure what semantics we would want to have.
Allow default methods to "see" defaulted associated types; in return, if you override any defaulted associated types, you have to override all defaulted methods.
What about a compromise: if default method mentions the associated type in it's signature, then this type is allowed to be default and then, if you override such type, only those methods that use the type in signature would see the type being default.
Would this still be unsound/surprising?
I hope this feature can be stabilised, it would help me out;
I was experimenting with rolling vector maths, trying to make it general enough to handle dimension checking / special point/normal types etc... it looks like this would be really useful for this. https://www.reddit.com/r/rust/comments/6i022j/traits_groupingsharing_constraints/dj4iwe1/?context=3
... I wanted to use 'helper traits' to simplify expressing a group of related types with some operations between them, without them necessarily 'belonging' to one type.
For my use case specific use case: I think it was most helpful for the trait to compute types that implementations must conform to; it was about letting any code that uses the trait also assume the whole set of bounds it defines. (whereas if I specify a 'where clause' in the trait, that merely tells users what else they need to manually specify)
it might look like overkill for the example in the thread, but the rest of my experiment is a lot messier than that
this is the sort of thing I'd want to be able to do:-
fn interp<X,Y>(x:X, (x0,y0):(X,Y), (x1,y1):(X,Y))->Y
where X:HasOperatorsWith<Y>
{ ((x-x0)/(x1-x0)) * (y1-y0) + y0
}
// Helper trait implies existance of operators between types Self,B
// such that addition/subtraction , multiplication/division produce intermediates
// but inverse operations cancel out.
// even differences may be other types, e.g. Unsigned minus Unsigned -> Signed
// Point minus Point -> Offset
pub trait HasOperatorsWith<B> {
type Diff=<Self as Sub<B>>::Output;
type ProdWith=<Self as Mul<B>>::Output;
type DivBy=<Self as Div<B>>::Output;
type Ratio=<Self as Div<Self>>::Output;
assume <Diff as Div<Diff>>::Output= Ratio;
type Square = <Self as Mul<B>> :: Ouput;
assume <ProdWith as Mul<DivBy> >::Output = Self // multiplication and division must be inverses
assume <Sum as Mul<DivBy> >::Output = Self
assume <Square as Sqrt<> > :: Output = Self
assume <Self as Add<Diff>> :: Output= Self // addition and subtraction must be inverses
// etc..
}
// implementor - so long as all those operators exist, consider 'HasOperatorsWith' to exist
// currently you must write another to instantiate it
impl <....> HasOperatorsWith<X> for Y where /* basically repeat all that above*/
maybe there's a nice way to say 'two functions are supposed to be inverses', e.g.
decltype(g(f(x:X)))==Y decltype(f(g(y:Y)))==X // f,g are unary functions, mutual inverses
decltype( gg(x, ff(x,y) ) )==X // ff, gg are binary operators that cancel out..
(any advice on how parts of this may already be possible is welcome, until 1 day ago I didn't realise you can do parts of that by placing bounds on the associated types.
Part of the problem is you are essentially re-writing code in a sort of LISP using the awkward angle-bracket type syntax; .. it's like we're going to end up with 'trait metaprogramming..'
Are there any RFCs for a C++ like 'decltype' ?
I also wish you had the old syntax for traits back as an option (just as you've got the option with 'where' to swap the order of writing bounds; part of the awkwardness is flipping the order in your head ... "Sum<Y,Output=Z> for X { fn sub(&self/ X / , Y)->Z {} }
if you could write these traits 'looking like' functions, that would also help e.g.
impl for X : Sum<Y,Output=Z> {..} / Mirrors the casting and calling syntax /
I have a issue similar to the one of @dobkeratops, where I'd like to provide a type alias, which is not just a default but should not be changed.
The code is
trait Dc<Elem> where Elem: Add + AddAssign + Sub + SubAssign + Neg + Rand {
type Sum = <Elem as Add>::Output;
[...]
}
It should not be possible to override Sum
.
Also, I ran into the restriction that @SimonSapin had with associated methods. I think it would be useful to have a reasonable solution to that (if one exists).
Would it be out of the question to stabilize a conservative implementation of this feature that doesn't let default functions assume the details of associated types, and lift restrictions later as allowed? Doing that should allow backwards-compatible changes in the future, and from what I've seen there don't seem to be any outstanding bugs preventing this from being stabilized. Those are also the semantics generic parameters on traits follow right now, so the behavior would be consistent with other parts of the language.
I would also prefer getting a conservative version of this stabilised as currently there is no way to backwards compatibly add new associated types.
I'm trying to make a trait with a function that returns an iterator and this is tripping me up:
trait Foo {}
trait Bar {}
trait Storage<T: Foo, U: Foo + Bar> {
type Item = U;
/// Get a value from the current key or None if it does not exist.
fn get(&self, id: &T) -> Option<U>;
/// Insert a value under the given key. Will overwrite the existing value.
fn insert(&self, id: &T, item: &U);
fn iter(&self) -> Iterator;
}
fn main () {
}
error: associated type defaults are unstable (see issue #29661)
--> src/main.rs:5:5
|
5 | type Item = U;
| ^^^^^^^^^^^^^^
https://play.rust-lang.org/?gist=1197424c5b9c3f16c6cdfc80fe744d5c&version=stable
I may have run into a couple of bugs with this associated type defaults, as I believe https://play.rust-lang.org/?gist=ffc94a727888539a99ccfe6d9965a217&version=nightly should compile.
It looks like the default is not actually being "applied" in the trait impl. I assume this is not intended?
Fixing that by explicitly applying (l.31 from working version link, below) runs into a second error, requiring an ?Sized bound (l.6 below).
Here is the working version: https://play.rust-lang.org/?gist=3e2f2f39e38815956aaefaf511add0c1&version=nightly
Credit to @durka for the workarounds. (Thank you!)
I just had one idea that would help ergonomics a lot. I'll call it "final associated types" for the purpose of this explanation. The idea is that some traits define associated types and sometimes, they use them as type parameters of other types. A notable example is the Future
trait.
trait Future {
type Item;
type Error;
// | This is long and annoying to write and read
// V
fn poll(&mut self) -> Poll<Self::Item, Self::Error>;
}
What would be nice is being able to write this:
trait Future {
type Item;
type Error;
// This is practically type alias.
// Can't be changed in trait impl, only used.
final type Poll = Poll<Self::Item, Self::Error>;
fn poll(&mut self) -> Self::Poll;
}
It would also help various cases when one has associated type called Error
and want's to return Result<T, Self::Error>
one could write this:
trait Foo {
type Error;
// It can be generic
final type Result<T> = Result<T, Self::Error>;
fn foo(&mut self) -> Self::Result<u32>;
}
What do you think? Should I write an RFC, or could this be part of something else?
@Kixunil For the first use case you can consider putting the type definition outside the trait:
trait Future {
type Item;
type Error;
fn poll(&mut self) -> FPoll<Self>;
}
type FPoll<F: Future> = Poll<F::Item, F::Error>;
@bgeron good idea. I still like Self::Poll
more, but maybe I will consider using type
outside of trait.
@Osspial as much as that'd be good, changing from a conservative to non-conservative assumption is a breaking change, as it would mean default methods which were previously available are now not available.
I'm for stabilizing this as well, but we need good semantics first.
Here is a workaround I developed for https://github.com/serde-rs/serde/pull/1354 and worked successfully for my use case. It doesn't support all possible uses for associated type defaults but it was sufficient for mine.
Suppose we have an existing trait MyTrait
and we would like to add an associated type in a non-breaking way i.e. with a default for existing impls.
// Placeholder for whatever bounds you would want to write on the
// associated type.
trait Bounds: Default + std::fmt::Debug {}
impl<T> Bounds for T where T: Default + std::fmt::Debug {}
trait MyTrait {
type Associated: Bounds = ();
//~~~~~~~~~~~~~~~~~~~~~~^^^^ unstable
}
struct T1;
impl MyTrait for T1 {}
struct T2;
impl MyTrait for T2 {
type Associated = String;
}
fn main() {
println!("{:?}", T1::Associated::default());
println!("{:?}", T2::Associated::default());
}
Instead we can write:
trait Bounds: Default + std::fmt::Debug {}
impl<T> Bounds for T where T: Default + std::fmt::Debug {}
trait MyTrait {
//type Associated: Bounds = ();
fn call_with_associated<C: WithAssociated>(callback: C) -> C::Value {
type DefaultAssociated = ();
callback.run::<DefaultAssociated>()
}
}
trait WithAssociated {
type Value;
fn run<A: Bounds>(self) -> Self::Value;
}
struct T1;
impl MyTrait for T1 {
// type Associated = ();
}
struct T2;
impl MyTrait for T2 {
// type Associated = String;
fn call_with_associated<C: WithAssociated>(callback: C) -> C::Value {
callback.run::<String>()
}
}
fn main() {
struct Callback;
impl WithAssociated for Callback {
type Value = ();
fn run<A: Bounds>(self) -> Self::Value {
// code uses the "associated type" A
println!("Associated = {:?}", A::default());
}
}
T1::call_with_associated(Callback);
T2::call_with_associated(Callback);
}
I believe I have ran into a bug with this feature. Here is a minimal example to reprodue it:
trait Foo {
type Inner = ();
type Outer: Into<Self::Inner>;
}
impl Foo for () {
// With this, it compiles:
// type Inner = ();
type Outer = ();
}
Although Inner
is given with ()
as its default, the example will not compile unless you specify type Inner = ();
.
Edit: more precisely, it gives this error:
error[E0277]: the trait bound `<() as Foo>::Inner: std::convert::From<()>` is not satisfied
--> src/lib.rs:8:6
|
8 | impl Foo for () {
| ^^^ the trait `std::convert::From<()>` is not implemented for `<() as Foo>::Inner`
|
= help: the following implementations were found:
<T as std::convert::From<T>>
= note: required because of the requirements on the impl of `std::convert::Into<<() as Foo>::Inner>` for `()`
It seems like the associated type default is only applied after the trait bound is evaluated. Is this expected?
Dumping for future reference:
#![feature(associated_type_defaults)]
trait A {
type B = Self::C;
type C = Self::B;
}
impl A for () {}
fn main() {
let x: <() as A>::B;
}
==>
error[E0275]: overflow evaluating the requirement `<() as A>::B`
thread '<unnamed>' panicked at 'Metadata module not compiled?', src/libcore/option.rs:1038:5
note: Run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
error: aborting due to previous error
RFC https://github.com/rust-lang/rfcs/pull/2532 is now merged and specifies how #![feature(associated_type_defaults)]
will behave moving forward. As a result, I will change the issue description and hide outdated and resolved comments so as to reduce confusion.
In the following example, I was trying to use associated types to clean up programming against a generic trait. It seems to work fine for the "Self::Input" but doesn't work for "Self::Output". I don't know if this is a bug, or if my expectation isn't intended, but I at least wanted to share this in case something needs to be changed.
#![feature(associated_type_defaults)]
pub trait Component<InputT, OutputT> {
type Input = InputT;
type Output = OutputT;
fn input(value: Self::Input);
fn result() -> Self::Output;
}
pub struct Reciever;
impl<'a> Component<&'a [u8], Vec<u8>> for Reciever {
fn input(_value: Self::Input) {}
fn result() -> Self::Output { Vec::<u8>::new() }
}
========
error[E0308]: mismatched types
--> src/lib.rs:15:35
|
15 | fn result() -> Self::Output { Vec::<u8>::new() }
| ------------ ^^^^^^^^^^^^^^^^ expected associated type, found struct `std::vec::Vec`
| |
| expected `<Reciever as Component<&'a [u8], std::vec::Vec<u8>>>::Output` because of return type
|
= note: expected type `<Reciever as Component<&'a [u8], std::vec::Vec<u8>>>::Output`
found type `std::vec::Vec<u8>`
RFC 2532 still needs to be implemented as part of this, can anyone from @rust-lang/compiler write up mentoring instructions?
EDIT: Actually I might have figured most of this out already
Regarding unresolved question 2:
In https://github.com/rust-lang/rust/pull/61812, I wrote an additional test for similar cycles in assoc. consts, which looks like this and works as-is on stable Rust:
// run-pass
// Cyclic assoc. const defaults don't error unless *used*
trait Tr {
const A: u8 = Self::B;
const B: u8 = Self::A;
}
// This impl is *allowed* unless its assoc. consts are used
impl Tr for () {}
// Overriding either constant breaks the cycle
impl Tr for u8 {
const A: u8 = 42;
}
impl Tr for u16 {
const B: u8 = 0;
}
impl Tr for u32 {
const A: u8 = 100;
const B: u8 = 123;
}
fn main() {
assert_eq!(<u8 as Tr>::A, 42);
assert_eq!(<u8 as Tr>::B, 42);
assert_eq!(<u16 as Tr>::A, 0);
assert_eq!(<u16 as Tr>::B, 0);
assert_eq!(<u32 as Tr>::A, 100);
assert_eq!(<u32 as Tr>::B, 123);
}
The RFC does not mention how defaults in trait objects are supposed to behave when the default type mentions the Self
type. For example, here:
trait Tr {
type Ty = &'static Self;
}
If an object type like dyn Tr
is created, what is Ty
supposed to be?
Defaults in type parameters bail out in this case and require the user to specify a type explicitly. We could do the same here.
EDIT: After a conversation on Discord it looks like this should go with the same solution as default type parameters (bail out and require the user to specify the type)
Has any work been going on regarding this issue since #61812?
Not to my knowledge, no
Is the checklist in the first post up to date? If not, what are the most important blockers right now?
It is up to date
A T-lang backlog bonanza meeting discussed this today, and we felt uncertain about the current status of this feature. There's a long list of tasks in the description, many of which are unchecked, but it's unclear whether that list represents an accurate current summary of the state here. Would someone familiar with the implementation be up for posting an updated summary, which we might uplift into the issue description?
That would provide context for making a triage decision of whether this is blocked on implementation or design concerns (or both).
Just an FYI if anyone needs something now in stable rust that provides associated type defaults / default associated types, my supertrait crate can do it :)
Any ETA on when we might see this in Stable? Next year perhaps?
Any ETA on when we might see this in Stable? Next year perhaps?
This is a tracking issue, a better place to ask this is on Zulip. ETA is impossible to give as there's still some unresolved questions about this feature and how it should be implemented that need to be resolved before someone starts working on it - check the linked issues and offer suggestions if you have any in the meantime.
If anyone wants to pick part of this up, I think the biggest thing missing is support for dyn
trait objects. That is, this should compile:
#![feature(associated_type_defaults)]
#![allow(unused)]
trait Trait {
type Foo = u8;
fn foo(&self) -> Self::Foo;
}
fn dyn_with_default_ty(a: &dyn Trait) -> u8 {
a.foo()
}
Currently it fails with E0191 "the value of the associated type Foo
in Trait
must be specified"
This can probably reuse a lot of the same logic as for generics T: Trait
(introduced in the original PR #61812), which should sidestep a lot of bugs. Many of the tests in test/ui/associated-types/default*
can probably be duplicated to test defaults with dyn
.
Other than that, @nikomatsakis (or others) what is meant by Correct defaults in impls (const)
in the top post? Does that just mean adding tests for this:
trait Trait {
type Foo = u8;
// currently "associated type defaults can't be assumed inside the trait defining them"
const Bar: Self::Foo = 100;
}
Which AIUI is rejected by the RFC, meaning the compiler handles it correctly.
@Centril I don't know if you are still active, but maybe you have some insight as the author of that incredibly complete RFC :)
This is a tracking issue for the RFC "Associated type defaults" (rust-lang/rfcs#2532) under the feature gate
#![feature(associated_type_defaults)]
.The associated item RFC included the ability to provide defaults for associated types, with some tricky rules about how that would influence defaulted methods.
The early implementation of this feature was gated, because there is a widespread feeling that we want a different semantics from the RFC -- namely, that default methods should not be able to assume anything about associated types. This is especially true given the specialization RFC, which provides a much cleaner way of tailoring default implementations.
The new RFC, rust-lang/rfcs#2532, specifies that this should be the new semantics but has not been implemented yet. The existing behavior under
#![feature(associated_type_defaults)]
is buggy and does not conform to the new RFC. Consult it for a discussion on changes that will be made.Steps:
Unresolved questions:
Test checklist
Originally created as a comment on #61812
type Foo = u8;
)dyn Trait
)dyn Trait<Foo = u16>
to thatu8
by invoking some method etcdyn Trait<Foo = u16>
)dyn Trait<Foo = u16>
to thatu8
by invoking some method etctype Foo = u8; type Bar;
)dyn Trait
) -- errorFoo
is specified (dyn Trait<Foo = u16>
) -- errorBar
is specified (dyn Trait<Bar = u32>
) -- ok, checkFoo
defaults tou8
dyn Trait<Foo = u16, Bar = u32>
) -- oktype Foo = u8; type Bar = Vec<Self::Foo>
)dyn Trait
) -- errorFoo
is specified (dyn Trait<Foo = u16>
) -- unclear, maybe an error?Bar
is specified (dyn Trait<Bar = u32>
) -- unclear, maybe an error?dyn Trait<Foo = u16, Bar = u32>
) -- oktype Foo = Self::Bar; type Bar = Self::Foo
)dyn Trait
)Foo
is specified (dyn Trait<Foo = u16>
)Bar
is specified (dyn Trait<Bar = u32>
)dyn Trait<Foo = u16, Bar = u32>
)type Foo = Vec<Self::Bar>; type Bar = Box<Self::Foo>;
)dyn Trait
)Foo
is specified (dyn Trait<Foo = u16>
)Bar
is specified (dyn Trait<Bar = u32>
)dyn Trait<Foo = u16, Bar = u32>
)type Foo = u8;
(defaults-in-other-trait-items.rs
)default type Foo = u8
, cannot rely on that internally (defaults-specialization.rs
)type Foo = u8
, cannot rely on that internally (defaults-specialization.rs
)type Foo = u8;
)associated-types/associated-types-overridden-default.rs
)impl Trait { }
) (associated-types/issue-54182-2.rs
)impl Trait { type Foo = u16; }
) (issue-54182-1.rs
)type Foo = u8; type Bar;
)impl Trait { }
) -- errorFoo
is specified (impl Trait { type Foo = u16; }
) -- errorBar
is specified (impl Trait { type Bar = u32; }
) -- okimpl Trait { type Foo = u16; type Bar = u32; }
) -- oktype Foo = u8; type Bar = Vec<Self::Foo>
) --defaults-in-other-trait-items-pass.rs
,defaults-in-other-trait-items-fail.rs
impl Trait { }
)Foo
is specified (impl Trait { type Foo = u16; }
)Bar
is specified (impl Trait { type Bar = u32; }
)impl Trait { type Foo = u16; type Bar = u32; }
)type Foo = Self::Bar; type Bar = Self::Foo
) --defaults-cyclic-fail.rs
,defaults-cyclic-pass.rs
impl Trait { }
)Foo
is specified (impl Trait { type Foo = u16; }
)Bar
is specified (impl Trait { type Bar = u32; }
)impl Trait { type Foo = u16; type Bar = u32; }
)type Foo = Vec<Self::Bar>; type Bar = Box<Self::Foo>;
)impl Trait { }
)Foo
is specified (impl Trait { type Foo = u16; }
)Bar
is specified (impl Trait { type Bar = u32; }
)impl Trait { type Foo = u16; type Bar = u32; }
)Foo
is specifiedBar
is specifiedFoo
is specifiedBar
is specifieddefaults-cyclic-fail.rs
,defaults-cyclic-pass.rs
impl Trait { }
)Foo
is specifiedBar
is specifiedFoo
is specifiedBar
is specifiedFoo
is specifiedBar
is specifieddefaults-suitability.rs
)type
in trait body, bound appears on the itemtype
in trait body, type not wftype
in trait body, bound appears as trait where clausedefault type
in impl, bound appears on the itemtype
in impl, bound appears on the itemtype
in default impl, bound appears on the itemtype
in trait body, conditionally wf depending on another defaulttype
in trait body, depends on another default whose bounds suffice