52 KiB
- Feature Name:
associated_type_defaults
- Start Date: 2018-08-27
- RFC PR: rust-lang/rfcs#2532
- Rust Issue: rust-lang/rust#29661
Summary
Resolve the design of associated type defaults,
first introduced in RFC 192,
such that provided methods and other items may not assume type defaults.
This applies equally to default
with respect to specialization.
Finally, dyn Trait
will assume provided defaults and allow those to be elided.
Motivation
As discussed in the background and mentioned in the summary, associated type defaults were introduced in RFC 192. These defaults are valuable for a few reasons:
-
You can already provide defaults for
const
s andfn
s. Allowingtype
s to have defaults adds consistency and uniformity to the language, thereby reducing surprises for users. -
Associated
type
defaults intrait
s simplify the grammar, allowing the grammar oftrait
s them to be more in line with the grammar ofimpl
s. In addition, this bringstrait
s more in line withtype
aliases.
The following points were also noted in RFC 192, but we expand upon them here:
-
Most notably, type defaults allow you to provide more ergonomic APIs.
For example, we could change proptest's API to be:
trait Arbitrary: Sized + fmt::Debug { type Parameters: Default = (); fn arbitrary_with(args: Self::Parameters) -> Self::Strategy; fn arbitrary() -> Self::Strategy { Self::arbitrary_with(Default::default()) } type Strategy: Strategy<Value = Self>; }
Being able to say that the default of
Parameters
is()
means that users, who are not interested in this further detail, may simply ignore specifyingParameters
.The inability of having defaults results in an inability to provide APIs that are both a) simple to use, and b) flexible / customizable. By allowing defaults, we can have our cake and eat it too, enabling both a) and b) concurrently.
-
Type defaults also aid in API evolution. Consider a situation such as
Arbitrary
from above; The API might have originally been:trait Arbitrary: Sized + fmt::Debug { fn arbitrary() -> Self::Strategy; type Strategy: Strategy<Value = Self>; }
with an implementation:
impl Arbitrary for usize { fn arbitrary() -> Self::Strategy { 0..100 } type Strategy = Range<usize>; }
By allowing defaults, we can transition to this more flexible API without breaking any consumers by simply saying:
trait Arbitrary: Sized + fmt::Debug { type Parameters: Default = (); fn arbitrary() -> Self::Strategy { Self::arbitrary_with(Default::default()) } fn arbitrary_with(_: Self::Parameters) -> Self::Strategy { Self::arbitrary() // This co-recursive definition will blow the stack. // However; since we can assume that previous implementors // actually provided a definition for `arbitrary` that // can't possibly reference `arbitrary_with`, we are OK. // You would only run into trouble for new implementations; // but that can be dealt with in documentation. } type Strategy: Strategy<Value = Self>; }
The implementation
Arbitrary for usize
remains valid even after the change.
Guide-level explanation
Background and The status quo
Let's consider a simple trait with an associated type and another item (1):
trait Foo {
type Bar;
const QUUX: Self::Bar;
fn wibble(x: Self::Bar) -> u8;
}
Ever since RFC 192, Rust has been capable of assigning default types to associated types as in (2):
#![feature(associated_type_defaults)]
trait Foo {
type Bar = u8;
const QUUX: Self::Bar = 42u8;
fn wibble(x: Self::Bar) -> u8 { x }
}
However, unlike as specified in RFC 192, which would permit (2), the current implementation rejects (2) with the following error messages (3):
error[E0308]: mismatched types
--> src/lib.rs:6:29
|
6 | const QUUX: Self::Bar = 42u8;
| ^^^^ expected associated type, found u8
|
= note: expected type `<Self as Foo>::Bar`
found type `u8`
error[E0308]: mismatched types
--> src/lib.rs:8:37
|
8 | fn wibble(x: Self::Bar) -> u8 { x }
| -- ^ expected u8, found associated type
| |
| expected `u8` because of return type
|
= note: expected type `u8`
found type `<Self as Foo>::Bar`
The compiler rejects snippet (2) to preserve the soundness of the type system. It must be rejected because a user might write (4):
struct Bar { ... }
impl Foo for Bar {
type Bar = Vec<u8>;
}
Given snippet (4), Self::Bar
will evaluate to Vec<u8>
,
which is therefore the type of <Bar as Foo>::QUUX
.
However, we have not given a different value for the constant,
and so it must be 42u8
, which has the type u8
.
Therefore, we have reached an inconsistency in the type system:
<Bar as Foo>::QUUX
is of value 42u8
, but of type Vec<u8>
.
So we may accept either impl Foo for Bar
as defined in (4),
or the definition of Foo
as in (2), but not both.
RFC 192 solved this dilemma by rejecting the implementation and insisting that if you override one associated type, then you must override all other defaulted items. Or stated in its own words:
- If a trait implementor overrides any default associated types, they must also override all default functions and methods.
- Otherwise, a trait implementor can selectively override individual default methods/functions, as they can today.
Meanwhile, as we saw in the error message above (3),
the current implementation takes the alternative approach of accepting
impl Foo for Bar
(4) but not the definition of Foo
as in (2).
Changes in this RFC
In this RFC, we change the approach in RFC 192 to the currently implemented approach. Thus, you will continue to receive the error message above and you will be able to provide associated type defaults.
With respect to specialization, the behaviour is the same. That is, if you write (5):
#![feature(specialization)]
trait Foo {
type Bar;
fn quux(x: Self::Bar) -> u8;
}
struct Wibble<T>;
impl<T> Foo for Wibble<T> {
default type Bar = u8;
default fn quux(x: Self::Bar) -> u8 { x }
}
The compiler will reject this because you are not allowed to assume,
just like before, that x: u8
. The reason why is much the same as
we have previously discussed in the background.
One place where this proposal diverges from what is currently implemented is with respect to the following example (6):
#![feature(associated_type_defaults)]
trait Foo {
type Bar = usize;
fn baz(x: Self::Bar) -> usize;
}
impl<T> Foo for Vec<T> {
fn baz(x: Self::Bar) -> usize { x }
}
In the current implementation, (6) is rejected because the compiler will not
let you assume that x
is of type usize
. But in this proposal, you would be
allowed to assume this. To permit this is not a problem because Foo for Vec<T>
is not further specializable since Bar
in the implementation has not been
marked as default
.
Trait objects
Another divergence in this RFC as compared to the current implementation is with respect to trait objects. Currently, if you write (7):
trait Foo {
type Bar = u8;
fn method(&self) -> Self::Bar;
}
type Alpha = Box<dyn Foo>;
the compiler will reject it with (8):
error[E0191]: the value of the associated type `Bar` (from the trait `Foo`) must be specified
--> src/lib.rs:8:17
|
4 | type Bar = u8;
| -------------- `Bar` defined here
...
8 | type Alpha = Box<dyn Foo>;
| ^^^^^^^ associated type `Bar` must be specified
With this RFC however, the error in (8) will disappear and (7) will be accepted.
That is, Box<dyn Foo>
is taken as equivalent as Box<dyn Foo<Bar = u8>>
.
If we complicate the situation slightly and introduce another associated type
Baz
which refers to Bar
in its default, the compiler will still let us
elide specifying the defaults (9):
trait Foo {
type Bar = u8;
type Baz = Vec<Self::Bar>;
fn method(&self) -> (Self::Bar, Self::Baz);
}
type Alpha = Box<dyn Foo>;
// -------
// Same as: `dyn Foo<Bar = u8, Baz = Vec<u8>>`.
type Beta = Box<dyn Foo<Bar = u16>>;
// ------------------
// Same as: `dyn Foo<Bar = u16, Baz = Vec<u16>>`.
Note that in Beta
, Bar
was specified but Baz
was not.
The compiler can infer that Baz
is Vec<u16>
since Self::Bar = u16
and
Baz = Vec<Self::Bar>
.
With these changes, we consider the design of associated type defaults to be finalized.
Reference-level explanation
The proposal makes no changes to the dynamic semantics and the grammar of Rust.
Static semantics
This section supersedes RFC 192 with respect to associated type defaults.
Associated types can be assigned a default type in a trait
definition:
trait Foo {
type Bar = $default_type;
$other_items
}
Any item in $other_items
, which have any provided definitions,
may only assume that the type of Self::Bar
is Self::Bar
.
They may not assume that the underlying type of Self::Bar
is $default_type
.
This property is essential for the soundness of the type system.
When an associated type default exists in a trait
definition,
it need not be specified in the implementations of that trait
.
If implementations of that trait
do not make that associated type
available for specialization, the $default_type
may be assumed
in other items specified in the implementation.
If an implementation does make the associated type available for
further specialization, then other definitions in the implementation
may not assume the given underlying specified type of the associated type
and may only assume that it is Self::TheAssociatedType
.
This applies generally to any item inside a trait
.
You may only assume the signature of an item, but not any provided definition,
in provided definitions of other items.
For example, this means that you may not assume the value of an
associated const
item in other items with provided definition
in a trait
definition.
Interaction with dyn Trait<...>
- Let
σ
denote a well-formed type. - Let
L
denote a well-formed lifetime. - Let
X
refer to an object safetrait
.- Let
k
denote the number of lifetime parameters inX
. - Let
l
denote the number of type parameters inX
. - Let
m
where0 ≤ m ≤ l
denote the number of type parameters inX
without specified defaults. - Let
A
denote the set of associated types inX
. - Let
o = |A|
. - Let
D
whereD ⊆ A
denote set of associated types inX
with defaults. - Let
E = A \ D
.
- Let
Then, in a type of form (where m ≤ n ≤ l
):
dyn X<
L0, .., Lk,
σ0, .. σn,
A0 = σ_{n + 1}, .., Ao = σ_{n + o}
>
the associated types in E
must be bound in A0, .., Ao
whereas those in D
may be omitted selectively (i.e. omit zero, some, or all).
When inferring the types of the omitted projections in D
,
projections in the assigned defaults of types in D
will use the types in
A0, .., Ao
instead of the defaults specified in D
. For example, if given:
trait X {
type A0 = u8;
type A1 = Vec<Self::A0>;
}
then the type dyn X<A0 = u16>
is inferred to dyn X<A0 = u16, A1 = Vec<u16>>
as opposed to dyn X<A0 = u16, A1 = Vec<u8>>
.
Interaction with existential type
RFC 2071 defines a construct existential type Foo: Bar;
which is permitted
in associated types and results in an opaque type. This means that the nominal
type identity is hidden from certain contexts and only Bar
is extensionally
known about the type wherefore only the operations of Bar
is afforded.
This construct is sometimes written as type Foo = impl Bar;
in conversation
instead.
With respect to this RFC, the semantics of type Assoc = impl Bar;
inside a trait definition, where Assoc
is the name of the associated type,
is understood as what it means in terms of default impl ..
as discussed
in RFC 1210. What this means in concrete terms is that given:
trait Foo {
type Assoc = impl Bar;
...
}
the underlying type of Assoc
stays the same for all implementations which
do not change the default of Assoc
. The same applies to specializations.
With respect to type opacity, it is the same as that of existential type
.
Drawbacks
The main drawbacks of this proposal are that:
-
if you have implementations where you commonly would have needed to write
default { .. }
because you need to assume the type of an associated type default in a provided method, then the solution proposed in this RFC is less ergonomic.However, it is the contention of this RFC that such needs will be less common and that the nesting mechanism or other similar ideas will be sufficiently ergonomic for such cases. This is discussed below.
Rationale and alternatives
Alternatives
The main alternative is to retain the behaviour in RFC 192 such that you may assume the type of associated type defaults in provided methods. As noted in the drawbacks section, this would be useful for certain types of APIs. However, it is more likely than not that associated type defaults will be used as a mechanism for code reuse than for other constructs. As such, we consider the approach in this RFC to be more ergonomic.
Another alternative to the mechanism proposed in this RFC is to somehow track which methods rely on which associated types as well as constants. However, we have historically had a strong bias toward being explicit in signatures about such things, avoiding to infer them. With respect to semantic versioning, such an approach may also cause surprises for crate authors and their dependents alike because it may be difficult at glance to decide what the dependencies are. This in turn reduces the maintainability and readability of code.
Consistency with associated const
s
Consider the following valid example from stable Rust:
trait Foo {
const BAR: usize = 1;
fn baz() { println!("Hi I'm baz."); }
}
impl Foo for () {
fn baz() { println!("Hi I'm () baz."); }
}
As we can see, you are permitted to override baz
but leave BAR
defaulted.
This is consistent with the behaviour in this RFC in that it has the same
property: "you don't need to override all items if you override one".
Consistency and uniformity of any programming language is vital to make its learning easy and to rid users of surprising corner cases and caveats. By staying consistent, as shown above, we can reduce the cost to our complexity budget that associated type defaults incur.
Overriding everything is less ergonomic
We have already discussed this to some extent.
Another point to consider is that Rust code frequently sports traits such as
Iterator
and Future
that have many provided methods and few associated types.
While these particular traits may not benefit from associated type defaults,
many other traits, such as Arbitrary
defined in the motivation, would.
True API evolution by inferring in dyn Trait
While impl Trait
will not take associated type defaults into account,
dyn trait
will. This may seem inconsistent. However, it is justified by the
inherent difference in semantics between these constructs and by the goal set
out in the motivation to facilitate API evolution.
As an illustration, consider Iterator
:
trait Iterator {
type Item;
...
}
Currently, you may write:
fn foo() -> impl Iterator { 0..1 }
and when foo
is called, you will know nothing about Item
.
However, you cannot write:
fn bar() -> Box<dyn Iterator> { Box::new(0..1) }
since the associated type Item
is not specified.
In bar
, the type of Item
is unknown and so the compiler does not know how
to generate the vtable. As a result, an error is emitted:
L | fn bar() -> Box<dyn Iterator> { Box::new(0..1) }
| ^^^^^^^^^^^^ missing associated type `Item` value
If we introduced a default for Item
:
type Item = ();
then bar
would become legal under this RFC and so strictly more code than
today would be accepted.
Meanwhile, if impl Iterator
meant impl Iterator<Item = ()>
,
this would impose a stronger requirement on existing code where impl Iterator
is used and thus it would be a breaking change to the users of Iterator
.
For Iterator
, it would not be helpful to introduce a default for Item
.
However, for the purposes of API evolution, the value is not in assigning
defaults to the existing associated types of a trait. Rather, the value comes
from being able to add associated types without breaking dependent crates.
Due to the possible breakage of dyn Trait<..>
when adding an associated type
to Trait
, to truly achieve API evolution, defaults must be taken into account
and be inferable for dyn Trait
. The opposite is true for impl Trait
.
To facilitate API evolution, stronger requirements must not be placed on
impl Trait
and therefore defaults should not be taken into account.
Prior art
Haskell
As Rust traits are a form of type classes, we naturally look for prior art from were they first were introduced. That language, being Haskell, permits a user to specify associated type defaults. For example, we may write the following legal program:
{-# LANGUAGE TypeFamilies #-}
class Foo x where
type Bar x :: *
-- A default:
type Bar x = Int
-- Provided method:
baz :: x -> Bar x -> Int
baz _ _ = 0
data Quux = Quux
instance Foo Quux where
baz _ y = y
As in this proposal, we may assume that y :: Int
in the above snippet.
In this case, we are not assuming that Bar x
unifies with Int
in the class
.
Let's try to assume that now:
{-# LANGUAGE TypeFamilies #-}
class Foo x where
type Bar x :: *
-- A default:
type Bar x = Int
-- Provided method:
baz :: x -> Bar x -> Int
baz _ barX = barX
This snippet results in a type checking error (tested on GHC 8.0.1):
main.hs:11:16: error:
• Couldn't match expected type ‘Int’ with actual type ‘Bar x’
• In the expression: barX
In an equation for ‘baz’: baz _ barX = barX
• Relevant bindings include
barX :: Bar x (bound at main.hs:11:9)
baz :: x -> Bar x -> Int (bound at main.hs:11:3)
<interactive>:3:1: error:
The thing to pay attention to here is:
Couldn't match expected type ‘
Int
’ with actual type ‘Bar x
’
We can clearly see that the type checker is not allowing us to assume
that Int
and Bar x
are the same type.
This is consistent with the approach this RFC proposes.
To our knowledge, Haskell does not have any means such as default { .. }
to change this behaviour. Presumably, this is the case because Haskell
preserves parametricity thus lacking specialization, wherefore default { .. }
,
as suggested in the future possibilities,
might not carry its weight.
Idris
Idris has a concept it calls interface
s.
These resemble type classes in Haskell, and by extension traits in Rust.
However, unlike Haskell and Rust, these interface
s do not have the property
of coherence and will permit multiple implementations of the same interface.
Since Idris is language with full spectrum dependent types,
it does not distinguish between terms and types, instead, types are terms.
Therefore, there is really not a distinct concept called "associated type".
However, an interface
may require certain definitions to be provided
and this includes types. For example, we may write:
interface Iterator self where
item : Type
next : self -> Maybe (self, item)
implementation Iterator (List a) where
item = a
next [] = Nothing
next (x :: xs) = Just (xs, x)
Like in Haskell, in Idris, a function or value in an interface may be given a default definition. For example, the following is a valid program:
interface Foo x where
bar : Type
bar = Bool
baz : x -> bar
implementation Foo Int where
baz x = x == 0
However, if we provide a default for baz
in the interface
which assumes
the default value Bool
of bar
, as with the following example:
interface Foo x where
bar : Type
bar = Bool
baz : x -> bar
baz _ = True
then we run into an error:
Type checking .\foo.idr
foo.idr:6:13-16:
|
6 | baz _ = True
| ~~~~
When checking right hand side of Main.default#baz with expected type
bar x _
Type mismatch between
Bool (Type of True)
and
bar x _ (Expected type)
The behaviour here is exactly as in Haskell and as proposed in this RFC.
C++
In C++, it is possible to provide associated types and specialize them as well. This is shown in the following example:
#include <iostream>
#include <string>
template<typename T> struct wrap {};
template<typename T> struct foo { // Unspecialized.
using bar = int;
bar make_a_bar() { return 0; };
};
template<typename T> struct foo<wrap<T>> { // Partial specialization.
using bar = std::string;
bar make_a_bar() { return std::string("hello world"); };
};
int main() {
foo<void> a_foo;
std::cout << a_foo.make_a_bar() << std::endl;
foo<wrap<void>> b_foo;
std::cout << b_foo.make_a_bar() << std::endl;
}
You will note that C++ allows us to assume in both the base template class,
as well as the specialization, that bar
is equal to the underlying type.
This is because one cannot specialize any part of a class without specializing
the whole of it. It's equivalent to one atomic default { .. }
block.
Swift
One language which does have associated types and defaults but which does not have provided definitions for methods is Swift. As an example, we may write:
protocol Foo {
associatedtype Bar = Int
func append() -> Bar
}
struct Quux: Foo {
func baz() -> Bar {
return 1
}
}
However, we may not write:
protocol Foo {
associatedtype Bar = Int
func append() -> Bar { return 0 }
}
This would result in:
main.swift:4:23: error: protocol methods may not have bodies
func baz() -> Bar { return 0 }
Scala
Another language which allows for these kinds of type projections and defaults
for them is Scala. While Scala does not have type classes like Rust and Haskell
does, it does have a concept of trait
which can be likened to a sort of
incoherent "type class" system. For example, we may write:
trait Foo {
type Bar = Int
def baz(x: Bar): Int = x
}
class Quux extends Foo {
override type Bar = Int
override def baz(x: Bar): Int = x
}
There are a few interesting things to note here:
-
We are allowed to specify a default type
Int
forBar
. -
A default definition for
baz
may be provided. -
This default definition may assume the default given for
Bar
. -
However, we must explicitly state that we are overriding
baz
. -
If we change the definition of of
override type Bar
toDouble
, the Scala compiler will reject it.
Unresolved questions
1. When do suitability of defaults need to be proven?
Consider a trait Foo<T>
defined as:
trait Foo<T> {
type Bar: Clone = Vec<T>;
}
Let's also assume the following implementation of Clone
:
impl<T: Clone> Clone for Vec<T> { ... }
To prove that Vec<T>: Clone
, we must prove that T: Clone
.
However, Foo<T>
does not say that T: Clone
so is its definition valid?
If the suitability of Vec<T>
is checked where Foo<T>
is defined (1),
then we don't know that T: Clone
and so the definition must be rejected.
To make the compiler admit Foo<T>
, we would have to write:
trait Foo<T: Clone> {
type Bar: Clone = Vec<T>;
}
Now it is provable that T: Clone
so Vec<T>: Clone
which is what was required.
If instead the suitability of defaults are checked in impl
ementations (2),
then proving Vec<T>: Clone
would not be required in Foo<T>
's definition and
so then Foo<T>
would type-check. As a result, it would be admissible to write:
#[derive(Copy, Clone)]
struct A;
struct B;
impl Foo<A> for B {}
since Vec<A>: Clone
holds.
With condition (2), strictly more programs are accepted than with (1). It may be that useful programs are rejected if we enforce (1) rather than (2). However, it would also be the more conservative choice, allowing us to move towards (2) when necessary. As it is currently unclear what solution is best, this question is left unresolved.
2. Where are cycles checked?
Consider a program (playground):
#![feature(associated_type_defaults)]
trait A {
type B = Self::C; // B defaults to C,
type C = Self::B; // C defaults to B, and we have a cycle!
}
impl A for () {}
fn _foo() {
let _x: <() as A>::B;
}
// Removing this function will make the example compile.
fn main() {
let _x: <() as A>::B;
}
Currently, this results in a crash. This will need to be fixed.
At the very latest, impl A for () {}
should have been an error.
trait A {
type B = Self::C;
type C = Self::B;
}
impl A for () {} // This OK but shouldn't be.
If cycles are checked for in impl A for ()
, then it would be valid to write:
trait A {
type B = Self::C;
type C = Self::B;
}
impl A for () {
type B = u8; // The cycle is broken!
}
Alternatively, cycles could be checked for in A
's definition.
This is similar to the previous question in (1).
Future possibilities
This section in the RFC used to be part of the proposal. To provide context for considerations made in the proposal, it is recorded here.
Summary
Introduce the concept of default { .. }
groups in traits
and their implementations which may be used to introduce atomic units of
specialization (if anything in the group is specialized, everything must be).
These groups may be nested and form a tree of cliques.
Motivation
For default { .. }
groups
Finally, because we are making changes to how associated type defaults work
in this RFC, a new mechanism is required to regain the loss of expressive power
due to these changes. This mechanism is described in the section on
default { .. }
groups as alluded to in the summary.
These groups not only retain the expressive power due to RFC 192 but extend power such that users get fine grained control over what things may and may not be overridden together. In addition, these groups allow users to assume the definition of type defaults in other items in a way that preserves soundness.
Examples where it is useful for other items to assume the default of an associated type include:
-
A default method whose return type is an associated type:
/// "Callbacks" for a push-based parser trait Sink { fn handle_foo(&mut self, ...); default { type Output = Self; // OK to assume what `Output` really is because any overriding // must override both `Output` and `finish`. fn finish(self) -> Self::Output { self } } }
-
There are plenty of other examples in rust-lang/rust#29661.
-
Other examples where
default { .. }
would have been useful can be found in the tracking issue for specialization:-
https://github.com/rust-lang/rust/issues/31844#issuecomment-198853202
You can see
default { .. }
being used here. -
https://github.com/rust-lang/rust/issues/31844#issuecomment-230093545
-
https://github.com/rust-lang/rust/issues/31844#issuecomment-247867693
-
https://github.com/rust-lang/rust/issues/31844#issuecomment-263175793
-
https://github.com/rust-lang/rust/issues/31844#issuecomment-279350986
-
-
Encoding a more powerful
std::remove_reference
We can encode a more powerful version of C++'s
remove_reference
construct, which allows you to get the base type of a reference type recursively. Without default groups, we can get access to the base type like so:trait RemoveRef { type WithoutRef; } impl<T> RemoveRef for T { default type WithoutRef = T; } impl<'a, T: RemoveRef> RemoveRef for &'a T { type WithoutRef = T::WithoutRef; }
However, we don't have any way to transitively dereference to
&Self::WithoutRef
. With default groups we can gain that ability with:trait RemoveRef { type WithoutRef; fn single_ref(&self) -> &Self::WithoutRef; } impl<T> RemoveRef for T { default { type WithoutRef = T; fn single_ref(&self) -> &Self::WithoutRef { // We can assume that `T == Self::WithoutRef`. self } } } impl<'a, T: RemoveRef> RemoveRef for &'a T { type WithoutRef = T::WithoutRef; fn single_ref(&self) -> &Self::WithoutRef { // We can assume that `T::WithoutRef == Self::WithoutRef`. T::single_ref(*self) } }
We can then proceed to writing things such as:
fn do_stuff(recv: impl RemoveRef<WithoutRef: MyTrait>) { recv.single_ref().my_method(); }
Guide-level explanation
default
specialization groups
Note: Overlapping implementations, where one is more specific than the other, requires actual support for specialization.
Now, you might be thinking: - "Well, what if I do need to assume that my defaulted associated type is what I said in a provided method, what do I do then?". Don't worry; We've got you covered.
To be able to assume that Self::Bar
is truly u8
in snippets (2) and (5),
you may henceforth use default { .. }
to group associated items into atomic
units of specialization. This means that if one item in default { .. }
is
overridden in an implementation, then all all the items must be. An example (7):
struct Country(&'static str);
struct LangSec { papers: usize }
struct CategoryTheory { papers: usize }
trait ComputerScientist {
default {
type Details = Country;
const THE_DETAILS: Self::Details = Country("Scotland"); // OK!
fn papers(details: Self::Details) -> u8 { 19 } // OK!
}
}
// https://en.wikipedia.org/wiki/Emily_Riehl
struct EmilyRiehl;
// https://www.cis.upenn.edu/~sweirich/
struct StephanieWeirich;
// http://www.cse.chalmers.se/~andrei/
struct AndreiSabelfeld;
// https://en.wikipedia.org/wiki/Conor_McBride
struct ConorMcBride;
impl ComputerScientist for EmilyRiehl {
type Details = CategoryTheory;
// ERROR! You must override THE_DETAILS and papers.
}
impl ComputerScientist for StephanieWeirich {
const THE_DETAILS: Country = Country("USA");
fn papers(details: Self::Details) -> u8 { 86 }
// ERROR! You must override Details.
}
impl ComputerScientist for AndreiSabelfeld {
type Details = LangSec;
const THE_DETAILS: Self::Details = LangSec { papers: 90 };
fn papers(details: Self::Details) -> u8 { details.papers }
// OK! We have overridden all items in the group.
}
impl ComputerScientist for ConorMcBride {
// OK! We have not overridden anything in the group.
}
You may also use default { .. }
in implementations.
When you do so, everything in the group is automatically overridable.
For any items outside the group, you may assume their signatures,
but not the default definitions given. An example:
trait Fruit {
type Details;
fn foo();
fn bar();
fn baz();
}
struct Citrus<S> { species: S }
struct Orange<V> { variety: V }
struct Blood;
struct Common;
impl<S> Fruit for Citrus<S> {
default {
type Details = bool;
fn foo() {
let _: Self::Details = true; // OK!
}
fn bar() {
let _: Self::Details = true; // OK!
}
}
fn baz() { // Removing this item here causes an error.
let _: Self::Details = true;
// ERROR! You may not assume that `Self::Details == bool` here.
}
}
impl<V> Fruit for Citrus<Orange<V>> {
default {
type Details = u8;
fn foo() {
let _: Self::Details = 42u8; // OK!
}
}
fn bar() { // Removing this item here causes an error.
let _: Self::Details = true;
// ERROR! You may not assume that `Self::Details == bool` here,
// even tho we specified that in `Fruit for Citrus<S>`.
let _: Self::Details = 22u8;
// ERROR! Can't assume that it's u8 either!
}
}
impl Fruit for Citrus<Orange<Common>> {
default {
type Details = f32;
fn foo() {
let _: Self::Details = 1.0f32; // OK!
}
}
}
impl Fruit for Citrus<Orange<Blood>> {
default {
type Details = f32;
}
fn foo() {
let _: Self::Details = 1.0f32;
// ERROR! Can't assume it is f32.
}
}
So far our examples have always included an associated type.
However, this is not a requirement.
We can also group associated const
s and fn
s together or just fn
s.
An example:
trait Foo {
default {
const BAR: usize = 3;
fn baz() -> [u8; Self::BAR] {
[1, 2, 3]
}
}
}
trait Quux {
default {
fn wibble() {
...
}
fn wobble() {
...
}
// For whatever reason; The crate author has found it imperative
// that `wibble` and `wobble` always be defined together.
}
}
Case study
One instance where default groups could be useful to provide a more ergonomic API is to improve upon RFC 2500. The RFC proposes the following API:
trait Needle<H: Haystack>: Sized {
type Searcher: Searcher<H::Target>;
fn into_searcher(self) -> Self::Searcher;
type Consumer: Consumer<H::Target>;
fn into_consumer(self) -> Self::Consumer;
}
However, it turns out that usually, Consumer
and Searcher
are
the same underlying type. Therefore, we would like to save the user
from some unnecessary work by letting them elide parts of the required
definitions in implementations.
One might imagine that we'd write:
trait Needle<H: Haystack>: Sized {
type Searcher: Searcher<H::Target>;
fn into_searcher(self) -> Self::Searcher;
default {
type Consumer: Consumer<H::Target> = Self::Searcher;
fn into_consumer(self) -> Self::Consumer { self.into_searcher() }
}
}
However, the associated type Searcher
does not necessarily implement
Consumer<H::Target>
. Therefore, the above definition would not type check.
However, we can encode the above construct by rewriting it slightly, using the concept of partial implementations from RFC 1210:
default impl<H: Haystack> Needle for T
where Self::Searcher: Consumer<H::Target> {
default {
type Consumer = Self::Searcher;
fn into_consumer(self) -> Self::Consumer { self.into_searcher() }
}
}
Now we have ensured that Self::Searcher
is a Consumer<H::Target>
and therefore, the above definition will type check.
Having done this, the API has become more ergonomic because we can
let users define instances of Needle<H>
with half as many requirements.
default fn foo() { .. }
is syntactic sugar
In the section of changes to associated type defaults, snippet (5) actually indirectly introduced default groups of a special form, namely "singleton groups". That is, when we wrote:
impl<T> Foo for Wibble<T> {
default type Bar = u8;
default fn quux(x: Self::Bar) -> u8 { x }
}
this was actually sugar for:
impl<T> Foo for Wibble<T> {
default {
type Bar = u8;
}
default {
fn quux(x: Self::Bar) -> u8 { x }
}
}
We can see that these are equivalent since in the specialization RFC,
the semantics of default fn
were that fn
may be overridden in more
specific implementations. With these singleton groups, you may assume
the body of Bar
in all other items in the same group; but it just
happens to be the case that there are no other items in the group.
Nesting and a tree of cliques
In the summary, we alluded to the notion of groups being nested. However, thus far we have seen no examples of such nesting. This RFC does permit you do that. For example, you may write:
trait Foo {
default {
type Bar = usize;
fn alpha() -> Self::Bar {
0 // OK! In the same group, so we may assume `Self::Bar == usize`.
}
// OK; we can rely on `Self::Bar == usize`.
default const BETA: Self::Bar = 3;
default fn gamma() -> [Self::Bar; 4] {
// OK; we can depend on the underlying type of `Self::Bar`.
[9usize, 8, 7, 6]
}
/// This is rejected:
default fn delta() -> [Self::Bar; Self::BETA] {
// ERROR! we may not rely on not on `Self::BETA`'s value because
// `Self::BETA` is a sibling of `Self::gamma` which is not in the
// same group and is not an ancestor either.
[9usize, 8, 7]
}
// But this is accepted:
default fn delta() -> [Self::Bar; 3] {
// OK; we can depend on `Self::Bar == usize`.
[9, 8, 7]
}
default {
// OK; we can still depend on `Self::Bar == usize`.
const EPSILON: Self::Bar = 2;
fn zeta() -> [Self::Bar; Self::Epsilon] {
// OK; We can assume the value of `Self::EPSILON` because it
// is a sibling in the same group. We may also assume that
// `Self::Bar == usize` because it is an ancestor.
[42usize, 24]
}
}
}
}
struct Eta;
struct Theta;
struct Iota;
impl Foo for Eta {
// We can override `gamma` without overriding anything else because
// `gamma` is the sole member of its sub-group. Note in particular
// that we don't have to override `alpha`.
fn gamma() -> [Self::Bar; 4] {
[43, 42, 41, 40]
}
}
impl Bar for Theta {
// Since `EPSILON` and `zeta` are in the same group; we must override
// them together. However, we still don't have to override anything
// in ancestral groups.
const EPSILON: Self::Bar = 0;
fn zeta() -> [Self::Bar; Self::Epsilon] {
[]
}
}
impl Bar for Iota {
// We have overridden `Bar` which is in the root group.
// Since all other items are descendants of the same group as `Bar` is in,
// they are allowed to depend on what `Bar` is.
type Bar = u8;
... // Definitions for all the other items elided for brevity.
}
In graph theory, a set of a vertices, in a graph, for which each distinct pair of vertices is connected by a unique edge is said to form a clique. What the snippet above encodes is a tree of such cliques. In other words, we can visualize the snippet as:
┏━━━━━━━━━━━━━━━━━┓
┃ + type Bar ┃
┏━━━━━━━━━━━━━┃ + fn alpha ┃━━━━━━━━━━━━━━┓
┃ ┗━━━━━━━━━━━━━━━━━┛ ┃
┃ ┃ ┃ ┃
┃ ┃ ┃ ┃
▼ ▼ ▼ ▼
┏━━━━━━━━━━━━━━━┓ ┏━━━━━━━━━━━━━┓ ┏━━━━━━━━━━━━━┓ ┏━━━━━━━━━━━━━━━━━┓
┃ + const Beta ┃ ┃ + fn gamma ┃ ┃ + fn delta ┃ ┃ + const EPSILON ┃
┗━━━━━━━━━━━━━━━┛ ┗━━━━━━━━━━━━━┛ ┗━━━━━━━━━━━━━┛ ┃ + fn zeta ┃
┗━━━━━━━━━━━━━━━━━┛
Please pay extra attention to the fact that items in the same group may depend on each other's definitions as well as definitions of items that are ancestors (up the tree). The inverse implication holds for what you must override: if you override one item in a group, you must override all items in that groups and all items in sub-groups (recursively). As before, these limitations exist to preserve the soundness of the type system.
Nested groups are intended primarily expected to be used when there is one associated type, for which you want to define a default, coupled with a bunch of functions which need to rely on the definition of the associated type. This is a good mechanism for API evolution in the sense that you can introduce a new associated type, rely on it in provided methods, but still perform no breaking change.
Reference-level explanation
Grammar
Productions in this section which are not defined here are taken from parser-lalr.y.
Given:
trait_item : maybe_outer_attrs trait_item_leaf ;
trait_item_leaf
: trait_const
| trait_type
| trait_method
| item_macro
;
trait_const
: CONST ident maybe_ty_ascription maybe_const_default ';'
;
trait_type : TYPE ty_param ';' ;
trait_method : method_prefix method_common ';' | method_prefix method_provided ;
method_prefix : maybe_unsafe | CONST maybe_unsafe | maybe_unsafe EXTERN maybe_abi ;
method_provided : method_common inner_attrs_and_block ;
method_common
: FN ident generic_params fn_decl_with_self_allow_anon_params maybe_where_clause
;
The production trait_item
is changed into:
trait_item : maybe_outer_attrs trait_item_def ;
trait_item_def
: trait_default_group
| trait_default_singleton
| trait_const
| trait_type
| trait_method
| item_macro
;
trait_default_singleton : DEFAULT trait_item ;
trait_default_group : DEFAULT '{' trait_item* '}' ;
trait_type : TYPE ty_param ('=' ty_sum)? ';' ;
Given:
impl_item : attrs_and_vis impl_item_leaf ;
impl_item_leaf
: item_macro
| maybe_default impl_method
| maybe_default impl_const
| maybe_default impl_type
;
impl_const : item_const ;
impl_type : TYPE ident generic_params '=' ty_sum ';' ;
impl_method : method_prefix method_common ;
method_common
: FN ident generic_params fn_decl_with_self maybe_where_clause inner_attrs_and_block
;
The production impl_item
is changed into:
impl_item : attrs_and_vis impl_item_def ;
impl_item_def
: impl_default_singleton
| impl_default_group
| item_macro
| impl_method
| impl_const
| impl_type
;
impl_default_singleton : DEFAULT impl_item ;
impl_default_group : DEFAULT '{' impl_item* '}' ;
Note that associated type defaults are already in the grammar due to RFC 192 but we have specified them in the grammar here nonetheless.
Note also that default default fn ..
as well as default default { .. }
are
intentionally recognized by the grammar to make life easier for macro authors
even though writing default default ..
should never be written directly.
Desugaring
After macro expansion, wherever the production trait_default_singleton
occurs,
it is treated in all respects as, except for error reporting -- which is left up
to implementations of Rust, and is desugared to DEFAULT '{' trait_item '}'
.
The same applies to impl_default_singleton
.
In other words: default fn f() {}
is desugared to default { fn f() {} }
.
Semantics and type checking
Semantic restrictions on the syntax
According to the grammar, the parser will accept items inside default { .. }
without a body. However, such an item will later be rejected during type checking.
The parser will also accept visibility modifiers on default { .. }
(e.g. pub default { .. }
). However, such a visibility modifier will also be
rejected by the type checker.
Specialization groups
Implementations of a trait
as well as trait
s themselves may now
contain "specialization default groups" (henceforth: "group(s)")
as defined by the grammar.
A group forms a clique and is considered an atomic unit of specialization wherein each item can be specialized / overridden.
Groups may contain other groups - such groups are referred to as
"nested groups" and may be nested arbitrarily deeply.
Items which are not in any group are referred to as 0
-deep.
An item directly defined in a group which occurs at the top level of a
trait
or an impl
definition is referred to as being 1
-deep.
An item in a group which is contained in a 1
-deep group is 2
-deep.
If an item is nested in k
groups it is k
-deep.
A group and its sub-groups form a tree of cliques.
Given a group $g
with items $x_1, .. $x_n
, an item $x_j
in $g
can assume the definitions of $x_i, ∀ i ∈ { 1..n }
as well as any
definitions of items in $f
where $f
is an ancestor of $g
(up the tree).
Conversely, items in $g
may not assume the definitions of items in
descendant groups $h_i
of $g
as well as items which are grouped at all
or which are in groups which are not ancestors of $g
.
If an impl
block overrides one item $x_j
in $g
,
it also has to override all $x_i
in $g
where i ≠ j
as well as
all items in groups $h_i
which are descendants of $g
(down the tree).
Otherwise, items do not need to be overridden.
For example, you may write:
trait Foo {
default {
type Bar = u8;
fn baz() {
let _: Self::Bar = 1u8;
}
default {
const SIZE: usize = 3;
fn quux() {
let_: [Self::Bar; Self::SIZE] = [1u8, 2u8, 3u8];
}
}
}
}
impl Foo for () {
type Bar = Vec<u8>;
fn baz() {}
const SIZE: usize = 5;
fn quux() {}
}
Linting redundant default
s
When in source code (but not as a consequence of macro expansion),
any of the following occurs, a warn-by-default lint (redundant_default
)
will be emitted:
default default $item
// ^^^^^^^ warning: Redundant `default`
// hint: remove `default`.
default default {
// ^^^^^^^ warning: Redundant `default`
// hint: remove `default`.
...
}
default {
...
default $item
// ^^^^^^^ warning: Redundant `default`
// hint: remove `default`.
...
}
Drawbacks
The main drawbacks of this proposal are that:
-
default { .. }
is introduced, adding to the complexity of the language.However, it should be noted that token
default
is already accepted for use by specialization and fordefault impl
. Therefore, the syntax is only partially new.
Rationale and alternatives
Alternatives
One may consider mechanisms such as default(Bar, BAZ) { .. }
to give
more freedom as to which dependency graphs may be encoded.
However, in practice, we believe that the tree of cliques approach proposed
in this RFC should be more than enough for practical applications.
default { .. }
is syntactically light-weight
When you actually do need to assume the underlying default of an associated type
in a provided method, default { .. }
provides a syntax that is comparatively
not that heavy weight.
In addition, when you want to say that multiple items are overridable,
default { .. }
provides less repetition than specifying default
on
each item would. Thus, we believe the syntax is ergonomic.
Finally, default { .. }
works well and allows the user a good deal of control
over what can and can't be assumed and what must be specialized together.
The grouping mechanism also composes well as seen in
the section where it is discussed.
Tree of cliques is familiar
The "can depend on" rule is similar to the rule used to determine whether a
non-pub
item in a module tree is accessible or not.
Familiarity is a good tool to limit complexity costs.
Non-special treatment for methods
In this RFC we haven't given methods any special treatment.
We could do so by allowing methods to assume the underlying type
of an associated type and still be overridable without having to override
the type. However, this might lead to semantic breakage in the sense that
the details of an fn
may be tied to the definition of an associated type.
When those details change, it may also be prudent to change the associated type.
Default groups give users a mechanism to enforce such decisions.
Future work
where
clauses on default { .. }
groups
From our case study, we noticed that we had to depart from our trait
definition into a separate default impl..
to handle the conditionality
of Self::Searcher: Consumer<H::Target>
. However, one method to regain
the locality provided by having default { .. }
inside the trait
definition
is to realize that we could attach an optional where
clause to the group.
This would allow us to write:
trait Needle<H: Haystack>: Sized {
type Searcher: Searcher<H::Target>;
fn into_searcher(self) -> Self::Searcher;
default where
Self::Searcher: Consume<H::Target>
{
type Consumer: Consumer<H::Target> = Self::Searcher;
fn into_consumer(self) -> Self::Consumer { self.into_searcher() }
}
}
The defaults in this snippet would then be equivalent to the default impl..
snippet noted in the case study.
This default where $bounds
construct should be able to
subsume common cases where you only have a single default impl..
but provide comparatively better local reasoning.
However, we do not propose this at this stage because it is unclear how
common default impl..
will be in practice.