Potential RFC: Delegation

Pre-RFC: Delegation

Many times, it would be useful if we could delegate in rust - the delegation pattern is good and there are many other uses of it (like in the newtype idiom). Currently, all we can do is implement Deref. But this has several disadvantages:

  • If we wanted to actually implement Deref for what it's designed for, we can't
  • We have to inherit all of the methods
  • We can only do it once This is more like extending another class than delegation. So, here's the proposed syntax.
trait Foo {
	fn bar(&self);
}
struct Delegate;
impl Foo for Delegate {
	fn bar(&self) {
		println!("Hello, world!");
	}
}
struct Derived {
	delegate: Delegate
}
impl Foo by self.delegate for Derived {}

Some notes on this syntax:

  • I've chosen to use as, because it's the most fitting keyword already in the language, and adding another keyword (like by or from) will break things. as is unclear, so a weak keyword by can be used instead
  • As we can see, the delegate is a member of the struct, not another class. This allows things like dynamic delegation and modifying the delegate (imagine a Counter trait and struct, you'd need to update the counter)
  • Instead of a trait, we can also specify * to copy all methods.
  • Maybe the for and as should swap?
  • Note the curly brackets, as we see another use of this syntax with more control:
trait Foo {
	fn bar(&self);
	fn quux(&self);
}
struct Delegate;
impl Foo for Delegate {
	fn bar(&self) {
		println!("Hello, world!");
	}
	fn quux(&self) {
		println!("Hello, quux!");
	}
}
struct Derived {
	delegate: Delegate
}
impl Foo for Derived
where bar by self.delegate {
	fn quux(&self) {
		println!("Not delegated");
	}
}

This where clause is similar to that of a generic parameter list in that it just allows for more control/less ugliness.

Here's the exact syntax (EBNF):

DelegateImpl = "impl", (trait | "*") (as_clause | for_clause, where_clause);
trait = ? ident ?;
as_clause = "by", ? expression ?, for_clause;
where_clause = "where", function_name, "by", expression, ",", [where_clause];
for_clause = "for", struct;
struct = ? ident ?;
function_name = ? ident ?;

Motivations

Composition

Currently, there is no struct inheritance in rust - this is a good thing, as you usually favour composition over inheritance. But you still have to refer to the composed object - the methods don't compose. Now you could do:

struct Base;
impl Base {
	fn foo(&self) {
		println!("Base class");
	}
}
struct Derived {
	base: Base
}
impl * by self.base for Base {}
fn main() {
	let myDerived = Derived {base: Base};
	myDerived.foo();
}

Newtype

The newtype idiom is very common as we can do various things to a type - implementing a trait or just wrapping around to make a new type (Days and Months aren't the same even if they are both i64). But, like in composition, you still have to call .0 or implement Deref, neither of which you should have to do.

struct Base;
impl Base {
	println!("Base");
}
struct Days(Base);
impl * by Base for Days {}

Delegation pattern

If it looks like a duck and acts like a duck, it is a duck.

This is the basis of the delegation pattern: using public traits to specify your type, then having a private implementation of it and a public creator function. This way, people can easily modify your type and pass their own version without affecting the rest of the code, which doesn't care. We could do:

pub trait Duck {
	fn quack(&self);
	fn create_duck() -> Duck {
		DuckImpl
	}
}
struct DuckImpl;
impl Duck for DuckImpl {
	fn quack(&self) {
		println!("Quack.");
	}
}
/// `DivingDuck` is a duck and more!
struct DivingDuck;
impl Duck by DuckImpl for DivingDuck {}
impl DivingDuck {
	pub fn swim(&self) {
		println!("Dive!");
	}
}
1 Like

A still-open RFC in this area: https://github.com/rust-lang/rfcs/pull/2393

Would definitely be great for this to make progress. The most important part for me is that it finds a useful subset that leaves space to easily expand to more scenarios in the future.

1 Like

Disagree with this reasoning strongly. Using "as" like this does not read correctly/grammatically and just sounds like modifier soup. A new contextual keyword should be possible here like "via", "using", "by", etc. Any of those would make more sense than "as".

1 Like

Yeah, as is a bit unclear. I'll change that, it could be a weak keyword as it only appears in that specific context.