rust-lang / rfcs

RFCs for changes to Rust
https://rust-lang.github.io/rfcs/
Apache License 2.0
5.93k stars 1.57k forks source link

Idea: default types for traits #2211

Open canndrew opened 6 years ago

canndrew commented 6 years ago

Some types are only visible through the traits that they implement. Currently, when Rust cannot infer a type it will either raise an error or, in some unclearly-defined situations, default the type to ! (or ()). Perhaps we could allow defaulting in more situations and allow the defaulting to be trait-directed.

For example, the signature of Iterator::unzip is:

fn unzip<A, B, FromA, FromB>(self) -> (FromA, FromB) where
    FromA: Default + Extend<A>,
    FromB: Default + Extend<B>,
    Self: Iterator<Item = (A, B)>, 

Now suppose I write:

let (_, foo) = some_iterator.unzip();
...
for x in foo {
    ...
}

If I haven't touched foo anywhere else my code, then this won't compile. All rust knows about foo's type is that it must impl Default + Extend<X> + IntoIterator<Item=X>, and there may be lots of types that satisfy this requirement. However Vec<X> is, in some sense, the canonical type that satisfies these requirements - when iterated it gives back the exact same items that where .extend-ed into it, in the exact same order. Perhaps the compiler should be able to infer Vec<X> in this case. Perhaps the standard library should be able to specify Vec<X> as the inferred type in cases like this with a declaration like:

default<A> Default + Extend<A> + IntoIterator<Item=A> = Vec<A>;

Multiple default declarations could override each other by being more specific in much the same way that impl specialization works. When the compiler tries to default a type which satisfies some required bounds B, it looks for the most specific trait A <: B for which there is a default type, and uses that type if it satisfies B.

The current behaviour of defaulting to ! could be replaced by a declaration in libcore of:

default ?Sized = !;

There could also be default of (just for example):

default Default = ();

So, is this a good idea? Or is it terrible?

hanna-kruppe commented 6 years ago

My only thought on the matter:

However Vec is, in some sense, the canonical type that satisfies these requirements - when iterated it gives back the exact same items that where .extend-ed into it, in the exact same order.

This property is far from unique. Even in std there's VecDeque (which could be disregarded as being more complex and hence less canonical) and LinkedList (which can't be dismissed on the same grounds). I could still see an argument for Vec being the default sequence type (and in practice, I don't think many people would dispute that), but one that is less nice and clean-cut than just pointing at this property.

burdges commented 6 years ago

If you're in no_std land, then ArrayVec<?> sounds more natural. Right now, the ? remains ambiguous, but maybe not if we get VLAs and know the size. And ArrayVec might eventually become superfluous, giving just a VLA.

ExpHP commented 6 years ago

I wish this were a property of the functions rather than the known trait bounds. I feel like a default of Vec<_> should somehow be tied to Iterator::{collect, unzip, partition}.

Type parameter defaults for functions were accidentally supported in the grammar for some time and I'm sure I've written them a number of times expecting them to do something. I gather from that issue that it is not necessarily clear how these should behave, but without being able to find much discussion on it I'm not sure I see how they are any muddier than defaults for parameters in types and traits.


I guess the difference for structs and traits is that those defaults are applied each and every time we write out the type, regardless of context. Whereas in the case of functions, we're moreso hoping for the default to merely act as a "suggestion" to type inference.

leoyvens commented 6 years ago

@ExpHP See #2321

canndrew commented 6 years ago

One place this could be useful is in automatic-enuming of return types in futures. For example: say I have

.and_then(|x| {
    match x {
        true => some_future,
        false => some_other_future,
    }
})

This won't compile because some_future and some_other_future have different types. Instead we have to box them:

.and_then(|x| {
    match x {
        true => Box::new(some_future) as Box<Future<...>>,
        false => Box::new(some_other_future) as Box<Future<...>>,
    }
})

However this does an unnecessary allocation. What we really want is to create an anonymous 2-variant enum that implements Future. Using this RFC, and the anonymous enums RFC, and the generic-sized tuples RFC (extended to anonymous enums), we could make the default type for Future<Item=T, Error=E> + From<A0> + From<A1> + ... + From<An> where A*: Future<Item=T, Error=E> be an anonymous enum over A0 .. An which implements Future<Item = T, Error=E>.

burdges commented 6 years ago

If you're already doing a Box<Future> then an Either as impl Future works just as well, right? I suppose Either<impl Future,impl Future> even works.

canndrew commented 6 years ago

@burdges I guess so, yeah Would still be nice to have generic-arity anonymous enums for this though.

burdges commented 6 years ago

We could maybe unify the various Either types so the same Eithertype worked for numerous traits, including Iterator, Future, Fn*, Clone, *Hash*, some of Deref*, Borrow*, As*, and some important traits from elsewhere like Rng. If this exists, then one might build an anonify!(..) proc macro to automatically build Either trees.