Introduction
In a recent lang design meeting Petrochenkov outlined a design for delegation
based on a new reuse keyword. I unfortunately had to drop off early because of
a work conflict, so unfortunately I couldn’t participate in the discussion.
I've been thinking a little more about delegation since. Though I like the idea
of delegation and the scope at which it's applied, I feel like the reuse
proposal isn't quite where it needs to be yet. Kotlin’s by keyword (“provided
by”) also provides delegation, and I think it is closer to what a Rust feature
should feel like:
- For traits you’d use
impl..for..by(“trait impl foo is provided by bar”) - For impl blocks you'd use
impl..by(“inherent impl is provided by bar”) - For individual methods you’d use
fn..by(“function foo is provided by bar”)
Usually I try to provide some degree of analysis and detail in my posts. But I'm quite short on time, so the best I can provide is a big list of translated examples from RFC 3530 to show you what I think we could do instead.
Forwarding a trait implementation
Here we forward the Hash implementation from some inner type to the outer type.
// Setup
pub struct BTreeSet < T > {
inner : BTreeMap < T , ()>,
}
// 1. Before
impl < T : Hash > Hash for BTreeSet < T > {
fn hash < H : Hasher >( & self , state : & mut H ) {
self . inner . hash ( state)
}
}
// 2. Using the `reuse` keyword proposal from RFC 3530
impl < T : Hash > Hash for BTreeSet < T > {
reuse Hash :: hash { self . inner }
}
// 3. Using a Kotlin-inspired `by` keyword.
impl < T : Hash > Hash for BTreeSet < T > by self . inner; Forwarding to a differently named method
The by spelling below assumes Self::iter can resolve through Self = &'a BTreeSet<T> to BTreeSet::iter. This assumes some amount of auto-deref working at the path level. But it’s not too bad either; perhaps something like: “Check the reference first; if it isn’t there, check the referenced type.”
// Setup
pub type Iter < ' a , T > = btree_map:: Keys < ' a , T , ()>;
pub struct BTreeSet < T > {
inner : BTreeMap < T , ()>,
}
impl < T > BTreeSet < T > {
pub fn iter ( & self ) -> Iter < ' _ , T > {
self . inner . keys ()
}
}
// 1. Before
impl < ' a , T > IntoIterator for &' a BTreeSet < T > {
type Item = &' a T ;
type IntoIter = Iter < ' a , T >;
fn into_iter ( self ) -> Iter < ' a , T > {
self . iter ()
}
}
// 2. Using the `reuse` keyword proposal from RFC 3530
impl < ' a , T > IntoIterator for &' a BTreeSet < T > {
type Item = &' a T ;
type IntoIter = Iter < ' a , T >;
reuse BTreeSet ::< T >:: iter as into_iter;
}
// 3. Using a Kotlin-inspired `by` keyword
impl < ' a , T > IntoIterator for &' a BTreeSet < T > {
type Item = &' a T ;
type IntoIter = Iter < ' a , T >;
fn into_iter by Self :: iter;
} Forwarding one inherent method
Here we forward a single method from an inner type to the outer type.
// Setup
pub struct BTreeSet < T > {
inner : BTreeMap < T , ()>,
}
// 1. Before
impl < T > BTreeSet < T > {
pub fn len ( & self ) -> usize {
self . inner . len ()
}
}
// 2. Using the `reuse` keyword proposal from RFC 3530
impl < T > BTreeSet < T > {
pub reuse BTreeMap ::< T , ()>:: len { self . inner }
}
// 3. Using a Kotlin-inspired `by` keyword
impl < T > BTreeSet < T > {
pub fn len by self . inner;
} Forwarding multiple inherent methods
BTreeMap has 20 or so methods defined on it, but we’re only choosing to forward two of those to BTreeSet.
// Setup
pub struct BTreeSet < T > {
inner : BTreeMap < T , ()>,
}
// 1. Before: expanded from the delegation example
impl < T > BTreeSet < T > {
pub fn len ( & self ) -> usize {
self . inner . len ()
}
pub fn is_empty ( & self ) -> bool {
self . inner . is_empty ()
}
}
// 2. Using the `reuse` keyword proposal from RFC 3530
impl < T > BTreeSet < T > {
pub reuse BTreeMap ::< T , ()>::{ len, is_empty} { self . inner }
}
// 3. Using a Kotlin-inspired `by` keyword
impl < T > BTreeSet < T > {
pub fn len by self . inner;
pub fn is_empty by self . inner;
} Forwarding all inherent methods
RFC 3530 states: “the path prefixes in glob delegations can only refer to traits” (ref), which means that when delegating inherent methods each method must be individually delegated.
The by keyword is a bit more powerful and can batch delegate methods. This is
probably a case worth looking more closely at when doing a more thorough pass. I
feel like the by form is fairly unambiguous in what it should refer to here.
We’re not implementing any trait, so the only thing this could refer to is all
the inherent items present on BTreeSet::inner.
// Setup
pub struct BTreeSet < T > {
inner : BTreeMap < T , ()>,
}
// 1. Before
impl < T > BTreeSet < T > {
pub fn len ( & self ) -> usize {
self . inner . len ()
}
pub fn is_empty ( & self ) -> bool {
self . inner . is_empty ()
}
pub fn clear ( & mut self ) {
self . inner . clear ()
}
pub fn append ( & mut self , other : & mut Self )
where
T : Ord ,
{
self . inner . append ( & mut other. inner )
}
pub fn keys ( & self ) -> btree_map:: Keys < ' _ , T , ()> {
self . inner . keys ()
}
pub fn values ( & self ) -> btree_map:: Values < ' _ , T , ()> {
self . inner . values ()
}
// ... more forwarding methods omitted.
}
// 2. Using the `reuse` keyword proposal from RFC 3530
impl < T > BTreeSet < T > {
pub reuse BTreeMap ::< T , ()>::{
len,
is_empty,
clear,
append,
keys,
values,
// ... more forwarding methods omitted.
} {
self . inner
}
}
// 3. Using a Kotlin-inspired `by` keyword
impl < T > BTreeSet < T > by self . inner; Renaming a forwarded method
The by keyword always requires spelling out the export name and the import target, so renaming is mostly a matter of changing the export name.
// Setup
pub struct BTreeSet < T > {
inner : BTreeMap < T , ()>,
}
// 1. Before
impl < T > BTreeSet < T > {
pub fn contains < Q >( & self , value : & Q ) -> bool
where
T : Borrow < Q > + Ord ,
Q : Ord + ?Sized ,
{
self . inner . contains_key ( value)
}
}
// 2. Using the `reuse` keyword proposal from RFC 3530
impl < T > BTreeSet < T > {
pub reuse BTreeMap ::< T , ()>:: contains_key as contains { self . inner }
}
// 3. Using a Kotlin-inspired `by` keyword
impl < T > BTreeSet < T > {
pub fn contains by self . inner. contains_key;
} Forwarding an associated function
Path traversal through fields with by uses the . notation. Method selection
on Self uses regular path traversal in the form of Self::foo. This is not
too different from reuse, except that we’re opting to enable the use of Self
instead of always requiring fully qualified names. Though there is likely
nothing preventing RFC 3530 from later adding support for that too.
// Setup
pub struct BTreeSet < T > {
inner : BTreeMap < T , ()>,
}
impl < T > BTreeSet < T > {
pub fn new () -> Self {
Self {
inner : BTreeMap :: new (),
}
}
}
// 1. Before
impl < T > Default for BTreeSet < T > {
fn default () -> BTreeSet < T > {
BTreeSet :: new ()
}
}
// 2. Using the `reuse` keyword proposal from RFC 3530
impl < T > Default for BTreeSet < T > {
reuse BTreeSet ::< T >:: new as default ;
}
// 3. Using a Kotlin-inspired `by` keyword
impl < T > Default for BTreeSet < T > {
fn default by Self :: new;
} Projecting multiple operands
Here the delegation needs to delegate both self → self.inner and other →
other.inner. The RFC spells the rules out for this, so all we really have show
here is a different way of spelling the same thing.
// Setup
pub struct BTreeSet < T > {
inner : BTreeMap < T , ()>,
}
// 1. Before
impl < T : PartialEq > PartialEq for BTreeSet < T > {
fn eq ( & self , other : & BTreeSet < T >) -> bool {
self . inner . eq ( & other. inner )
}
}
// 2. Using the `reuse` keyword proposal from RFC 3530
impl < T : PartialEq > PartialEq for BTreeSet < T > {
reuse PartialEq :: eq { self . inner }
}
// 3. Using a Kotlin-inspired `by` keyword
impl < T : PartialEq > PartialEq for BTreeSet < T > by self . inner; Returning the wrapper and delegating a whole trait
The RFC explains why this notation automatically delegates both clone and
clone_from for reuse. Again: what I’m suggesting here is basically the same
thing, but using a different notation.
// Setup
pub struct BTreeSet < T > {
inner : BTreeMap < T , ()>,
}
// 1. Before
impl < T : Clone > Clone for BTreeSet < T > {
fn clone ( & self ) -> Self {
BTreeSet {
inner : self . inner . clone (),
}
}
fn clone_from ( & mut self , source : & Self ) {
self . inner . clone_from ( & source. inner );
}
}
// 2. Using the `reuse` keyword proposal from RFC 3530
reuse impl < T : Clone > Clone for BTreeSet < T > { self . inner }
// 3. Using a Kotlin-inspired `by` keyword
impl < T : Clone > Clone for BTreeSet < T > by self . inner ; Delegating a trait while overriding one method
The by variant omits type Item because it forwards the delegate's Iterator::Item unchanged.
// Setup
struct Wrapper < I > {
inner : I ,
}
// 1. Before
impl < I : Iterator > Iterator for Wrapper < I > {
type Item = I :: Item ;
fn next ( & mut self ) -> Option < Self :: Item > {
self . inner . next ()
}
fn size_hint ( & self ) -> ( usize , Option < usize >) {
( 0 , None )
}
fn count ( self ) -> usize {
self . inner . count ()
}
// Other forwarding methods omitted.
}
// 2. Using the `reuse` keyword proposal from RFC 3530
impl < I : Iterator > Iterator for Wrapper < I > {
type Item = I :: Item ;
reuse Iterator :: * { self . inner }
fn size_hint ( & self ) -> ( usize , Option < usize >) {
( 0 , None )
}
}
// 3. Using a Kotlin-inspired `by` keyword
impl < I : Iterator > Iterator for Wrapper < I > by self . inner {
fn size_hint ( & self ) -> ( usize , Option < usize >) {
( 0 , None )
}
} Choosing between multiple trait implementations
This is a fun case where we have to use an additional explicit by delegation on a trait method to be able to spell out the exact path we’re selecting:
<self.inner as Trait<()>>::foo is proposed syntax for qualifying the
projected inner field; it is not existing Rust syntax.
// Setup
trait Trait < T > {
fn foo ( & self );
}
struct Inner ;
impl Trait < i32 > for Inner {
fn foo ( & self ) {}
}
impl Trait <()> for Inner {
fn foo ( & self ) {}
}
struct Wrapper {
inner : Inner ,
}
// 1. Before
impl < T > Trait < T > for Wrapper {
fn foo ( & self ) {
<Inner as Trait <()> >:: foo ( & self . inner )
}
}
// 2. Using the `reuse` keyword proposal from RFC 3530
impl < T > Trait < T > for Wrapper {
reuse <Inner as Trait <()> >:: foo { self . inner }
}
// 3. Using a Kotlin-inspired `by` keyword
impl < T > Trait < T > for Wrapper {
fn foo by <self. inner as Trait <() >>:: foo;
} Delegating an associated function to the inner type
The by keyword can provide unambiguous lookup when delegating traits. impl Trait for Wrapper makes it clear we’re limiting our search for trait Trait. Then by providing by self.inner it’s now clear that we’re only interested in delegating to impl Trait for Inner. That makes any lookup of fn foo unambiguous. And in this case we can just delegate all methods at once, so the simple thing here does the right thing.
// Setup
trait Trait {
fn foo () -> usize ;
}
struct Inner ;
impl Trait for Inner {
fn foo () -> usize {
42
}
}
struct Wrapper {
inner : Inner ,
}
// 1. Before
impl Trait for Wrapper {
fn foo () -> usize {
<Inner as Trait >:: foo ()
}
}
// 2. Using the `reuse` keyword proposal from RFC 3530
impl Trait for Wrapper {
reuse <Inner as Trait >:: foo;
}
// 3. Using a Kotlin-inspired `by` keyword
impl Trait for Wrapper by self . inner; Example: FlatMap
The by variant omits type Item because it forwards the delegate's Iterator::Item unchanged.
// Setup
pub struct FlatMap < I , U , F >
where
I : Iterator ,
U : IntoIterator ,
F : FnMut ( I :: Item ) -> U ,
{
inner : Flatten < Map < I , F >>,
}
// 1. Before: representative forwarding methods
impl < I , U , F > Iterator for FlatMap < I , U , F >
where
I : Iterator ,
U : IntoIterator ,
F : FnMut ( I :: Item ) -> U ,
{
type Item = U :: Item ;
fn next ( & mut self ) -> Option < Self :: Item > {
self . inner . next ()
}
fn size_hint ( & self ) -> ( usize , Option < usize >) {
self . inner . size_hint ()
}
fn count ( self ) -> usize {
self . inner . count ()
}
// Other forwarding methods omitted.
}
// 2. Using the `reuse` keyword proposal from RFC 3530
impl < I , U , F > Iterator for FlatMap < I , U , F >
where
I : Iterator ,
U : IntoIterator ,
F : FnMut ( I :: Item ) -> U ,
{
type Item = U :: Item ;
reuse Iterator :: * { self . inner }
}
// 3. Using a Kotlin-inspired `by` keyword
impl < I , U , F > Iterator for FlatMap < I , U , F > by self . inner
where
I : Iterator ,
U : IntoIterator ,
F : FnMut ( I :: Item ) -> U ; API stability
Something that seems important to account for is API stability of inner types.
When delegating inherent methods with reuse there is no way to declare what
the signature ought to be, making it a potential semver hazard when upgrading.
With by that risk can be mitigated by allowing the full signature to be
spelled out if desired:
impl < T > BTreeSet < T > {
pub fn len by self . inner; // shorthand
pub fn len ( & self ) -> usize by self . inner; // full signature
} Using the full signature here would allow the delegation to provide an API guarantee, protecting against accidental breakage, but at the cost of being more verbose. Whether that tradeoff is one you want depends on what you’re doing. I expect the stdlib would probably always want it, but in my application code I probably wouldn’t.
Conclusion
I think what is most missing from the reuse keyword is a consistent feel with the rest of the language. One way to describe this is through edit distance, which itself translates to git diff noise. reuse feels very different from the rest of the language once you put items side by side.
To enhance/zoom in on what I mean, consider what a method might look like when implemented, when defined in a trait without a body, and when delegated. Using the by keyword, we get something where each variant feels like it has a shared design language:
// Using a Kotlin-inspired `by` keyword
fn len ( & self ) -> usize { … } // impl
fn len ( & self ) -> usize ; // trait def
fn len by self . inner ; // delegation The len method here is provided by delegating to the len method on inner. Now compare this with the reuse keyword. Delegating a function looks very different from implementing or defining it in a trait:
// Using the `reuse` keyword proposal from RFC 3530
fn len ( & self ) -> usize { … } // impl
fn len ( & self ) -> usize ; // trait def
reuse Foo ::< T , ()>:: len { self. inner } // delegation The same happens when we look at trait impls. Delegating a trait implementation should feel similar to implementing it by hand. Here again is the by version, which fits in naturally:
// Using a Kotlin-inspired `by` keyword
impl Foo for Bar { … } // trait impl
impl Foo for Bar by self . inner ; // trait delegation And the reuse version, which introduces a different item prefix:
// Using the `reuse` keyword proposal from RFC 3530
impl Foo for Bar { … } // trait impl
reuse impl Foo for Bar { self . inner } // trait delegation Does it make sense what I'm saying? I feel like by is quite beautiful here. It makes for satisfying one-liners that feel like a natural extension to the language, and I think it’s worth considering as an alternative to reuse. It also would pair nicely with the with-notation I’ve described in previous posts, as well as existing keywords like where and for we already use in Rust.
Note: I wish I had more time to work through all the rules involved here. Path disambiguation in particular would be worth spending time on. But I've got no time to spend, so I hope it's good enough at least to get the idea across.
Note: I want to reiterate how useful the work in RFC 3530 has been. The hardest part of any RFC is not defining the solution, but defining the problem and scope. I want to make sure that even though we might have different ideas about some of the solutions, we are in agreement about the problems.