rust-lang / rust

Empowering everyone to build reliable and efficient software.
https://www.rust-lang.org
Other
98.24k stars 12.71k forks source link

Tracking issue for RFC 2532, "Associated type defaults" #29661

Open aturon opened 9 years ago

aturon commented 9 years ago

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

SimonSapin commented 8 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?

aturon commented 8 years ago

@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.

SimonSapin commented 8 years ago

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?

aturon commented 8 years ago

@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).

aturon commented 8 years ago

(cc @nikomatsakis on that last comment)

nikomatsakis commented 8 years ago

@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. :)

sunjay commented 7 years ago

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...
}
nikomatsakis commented 7 years ago

@sunjay nobody is working on this, I think we are still unsure what semantics we would want to have.

Kixunil commented 7 years ago

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?

dobkeratops commented 7 years ago

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

dobkeratops commented 7 years ago

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 /

real-or-random commented 7 years ago

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).

Osspial commented 7 years ago

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.

WaDelma commented 6 years ago

I would also prefer getting a conservative version of this stabilised as currently there is no way to backwards compatibly add new associated types.

jtremback commented 6 years ago

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

U007D commented 6 years ago

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!)

Kixunil commented 6 years ago

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?

bgeron commented 6 years ago

@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>;
Kixunil commented 6 years ago

@bgeron good idea. I still like Self::Poll more, but maybe I will consider using type outside of trait.

daboross commented 6 years ago

@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.

dtolnay commented 6 years ago

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);
}
Centril commented 6 years ago

cc https://github.com/rust-lang/rfcs/pull/2532

whentze commented 5 years ago

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?

Centril commented 5 years ago

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
Centril commented 5 years ago

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.

Visic commented 5 years ago

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>`
jonas-schievink commented 5 years ago

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

jonas-schievink commented 5 years ago

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);
}
jonas-schievink commented 5 years ago

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)

mark-i-m commented 4 years ago

61812 has merged 🎉

ibraheemdev commented 3 years ago

Has any work been going on regarding this issue since #61812?

jonas-schievink commented 3 years ago

Not to my knowledge, no

Frizi commented 3 years ago

Is the checklist in the first post up to date? If not, what are the most important blockers right now?

jonas-schievink commented 3 years ago

It is up to date

Mark-Simulacrum commented 2 years ago

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).

sam0x17 commented 1 year ago

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 :)

Sajjon commented 10 months ago

Any ETA on when we might see this in Stable? Next year perhaps?

Caellian commented 10 months ago

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.

tgross35 commented 8 months ago

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.

tgross35 commented 8 months ago

@Centril I don't know if you are still active, but maybe you have some insight as the author of that incredibly complete RFC :)