New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tracking issue for `associated_type_defaults` #29661

Open
aturon opened this Issue Nov 6, 2015 · 23 comments

Comments

Projects
None yet
@aturon
Member

aturon commented Nov 6, 2015

This issue tracks the associated type default feature.

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.

Related issues

Feature requests and use cases.

  • #35986 is a feature request that trait objects should "default" to the default from the trait.

Bugs.

@SimonSapin

This comment has been minimized.

Contributor

SimonSapin commented Jan 25, 2016

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

This comment has been minimized.

Member

aturon commented Jan 28, 2016

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

  • Do not allow default methods to assume anything about defaulted associated types; in return, you can pick and choose which defaults to use in your impl.
  • Allow default methods to "see" defaulted associated types; in return, if you override any defaulted associated types, you have to override all defaulted methods. Otherwise, you could get unsound code (based on incorrect type assumptions).

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

This comment has been minimized.

Contributor

SimonSapin commented Jan 28, 2016

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

This comment has been minimized.

Member

aturon commented Jan 29, 2016

@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

This comment has been minimized.

Member

aturon commented Jan 29, 2016

(cc @nikomatsakis on that last comment)

@nikomatsakis

This comment has been minimized.

Contributor

nikomatsakis commented Feb 1, 2016

@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

This comment has been minimized.

Member

sunjay commented Feb 19, 2017

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

This comment has been minimized.

Contributor

nikomatsakis commented Feb 21, 2017

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

@Kixunil

This comment has been minimized.

Kixunil commented May 7, 2017

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

This comment has been minimized.

dobkeratops commented Jun 20, 2017

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

This comment has been minimized.

dobkeratops commented Jun 20, 2017

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

This comment has been minimized.

real-or-random commented Aug 23, 2017

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

This comment has been minimized.

Osspial commented Sep 19, 2017

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.

@cramertj cramertj referenced this issue Nov 7, 2017

Open

Allocator traits and std::heap #32838

5 of 12 tasks complete

@WaDelma WaDelma referenced this issue Nov 15, 2017

Closed

Metadata #301

@WaDelma

This comment has been minimized.

WaDelma commented Nov 15, 2017

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

This comment has been minimized.

jtremback commented Dec 20, 2017

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

This comment has been minimized.

Contributor

U007D commented Dec 31, 2017

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

This comment has been minimized.

Kixunil commented Feb 10, 2018

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

This comment has been minimized.

bgeron commented Feb 21, 2018

@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

This comment has been minimized.

Kixunil commented Feb 24, 2018

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

@daboross

This comment has been minimized.

Contributor

daboross commented Apr 1, 2018

@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

This comment has been minimized.

Member

dtolnay commented Aug 11, 2018

Here is a workaround I developed for serde-rs/serde#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

This comment has been minimized.

Contributor

Centril commented Aug 27, 2018

@whentze

This comment has been minimized.

Contributor

whentze commented Nov 19, 2018

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment