Another Rust spot: delegation
Yeah, I know: I lied. In the last post, I wrote, “Next time: given the file contents, can we diddle-about with word-like tokens in it without copying strings around?” and yet I’m not writing about that now. Sorry. Maybe next time. This seemed more important.
Anyway, here’s the deal: suppose you have a type Basic
which has a few methods and perhaps an implementation of a trait:
struct Basic {
some_field: usize,
}
impl Basic {
pub fn new() -> Basic { Basic { some_field: 4} }
pub fn foo(&self) -> usize { self.some_field - 1 }
pub fn bar(&self, i: usize) -> f64 { self.some_field as f64 / i as f64 }
pub fn baz(&mut self) -> usize { self.some_field }
}
trait A {
fn quux(&self) -> usize;
}
impl A for Basic {
fn quux(&self) -> usize { self.some_field }
}
And you have a type, Extended
, that has a field of type Basic
:
pub struct Extended {
inner: Basic,
_red_herring: usize,
}
impl Extended {
pub fn new() -> Extended {
Extended { inner: Basic::new(), _red_herring: 12 } }
}
What you want is for Extended
to support the same set of methods (and the trait) as Basic
, with the methods being delegated to the Basic
field.
There are, currently, three ways to deal with this:
Write a bunch of trivial methods for
Extended
like,pub fn foo(&self) -> { self.inner.foo() }
But that is rather tedious.
Wait for a compiler extension or change to the Rust language to support a “delegate” attribute or something. Like maybe RFC 292: allow delegating some methods from an trait impl to a field of a struct #292.
Now, I’m lazy, so that’s not a bad plan, but still, I would like to get some work done now.
Drag out some macro-fu, polish it up bright and shiny, and lay waste to every village and small township between here and the sea! Remember: pillage, then burn.
There are undoubtedly some weaknesses here, and it’s going to have to be a bit more verbose than the second option (and I won’t get a nap), but it’s what I’m doing. Otherwise, this wouldn’t be much of a blog post, would it?
The macro
Without further ado, here’s the macro:
#[macro_export]
macro_rules! delegate {
( $fld:ident : ) => {
};
( $fld:ident : $fcn:ident ( $( $a:ident : $at:ty ),* ) -> $r:ty ) => {
fn $fcn ( &self, $( $a : $at ),* ) -> $r {
(self.$fld).$fcn( $( $a ),* )
}
};
( $fld:ident : $fcn:ident ( $( $a:ident : $at:ty ),* ) -> $r:ty, $($rest:tt)* ) => {
fn $fcn ( &self, $( $a : $at ),* ) -> $r {
(self.$fld).$fcn( $( $a ),* )
}
delegate!($fld : $($rest)*);
};
( $fld:ident : pub $fcn:ident ( $( $a:ident : $at:ty ),* ) -> $r:ty ) => {
pub fn $fcn ( &self, $( $a : $at ),* ) -> $r {
(self.$fld).$fcn( $( $a ),* )
}
};
( $fld:ident : pub $fcn:ident ( $( $a:ident : $at:ty ),* ) -> $r:ty, $($rest:tt)* ) => {
pub fn $fcn ( &self, $( $a : $at ),* ) -> $r {
(self.$fld).$fcn( $( $a ),* )
}
delegate!($fld : $($rest)*);
};
( $fld:ident : mut $fcn:ident ( $( $a:ident : $at:ty ),* ) -> $r:ty ) => {
fn $fcn ( &mut self, $( $a : $at ),* ) -> $r {
(self.$fld).$fcn( $( $a ),* )
}
};
( $fld:ident : mut $fcn:ident ( $( $a:ident : $at:ty ),* ) -> $r:ty, $($rest:tt)* ) => {
fn $fcn ( &mut self, $( $a : $at ),* ) -> $r {
(self.$fld).$fcn( $( $a ),* )
}
delegate!($fld : $($rest)*);
};
( $fld:ident : pub mut $fcn:ident ( $( $a:ident : $at:ty ),* ) -> $r:ty ) => {
pub fn $fcn ( &mut self, $( $a : $at ),* ) -> $r {
(self.$fld).$fcn( $( $a ),* )
}
};
( $fld:ident : pub mut $fcn:ident ( $( $a:ident : $at:ty ),* ) -> $r:ty, $($rest:tt)* ) => {
pub fn $fcn ( &mut self, $( $a : $at ),* ) -> $r {
(self.$fld).$fcn( $( $a ),* )
}
delegate!($fld : $($rest)*);
};
}
Whew. I haven’t written that many funny characters since the last time I did Perl professionally.
This is actually a pretty simple recursive macro with a great number of repeated elements. A basic invocation would be:
delegate!{
inner:
foo() -> usize
}
This little guy produces:
fn foo(&self) -> usize { inner.foo() }
What the macro does is to take the name of a field as a first argument and a list of simplified method declarations as the remaining arguments, and then produces a sequence of method definitions that delegate the matching methods to the field.
The minimum information required to generate the definitions is:
The field that is the destination of the delegation (with this macro, it has to be a named field; structures with a single anonymous “.0” name aren’t supported),
The name of the method,
The types (and names) of the method’s arguments and return value, and
A couple of markers that I’ll get into below.
Since this is a simple syntax extension, all of those have to be present in the call to delegate
. Let me break down the macro.
The first rule is a simple default which expands to nothing if the body of the invocation is empty (or commented out).
The remainder of the rules are in four pairs:
One with no extra markers, as in the example above,
One with a
pub
marker, producing a method withpub
visibility,One with a
mut
marker, producing a method where the reference toself
is mutable, andOne with both
pub
andmut
markers.
Each of the pairs consists of a base case, producing a single method as an expansion, and a recursive case, producing a method and then re-invoking the macro on the next method type specification.
In each rule,
$fld
is an identifier, the field specifier (which only needs to be given once; it’s passed along to the recursive calls).$fcn
is another identifier and is the function name.$a
(an identifier) and$at
(a type) are the argument names and types of any method arguments (other thanself
).$r
is the return type of the method. I wonder if you can specify()
as the return type to get a procedure that does not return anything. Possibly another weakness of this monstrosity.
Finally, in the recursive rules, $rest
is the remainder of the macro invocation. To tell the truth, I’m not entirely sure what a tt
is or why you have to have a sequence of them for the recursive calls; I copied it from the example in the book. Yay, cargo-culting!
Finally, here are some examples of it in use.
impl Extended {
delegate!{
inner:
pub foo() -> usize,
pub bar(i:usize) -> f64,
pub mut baz() -> usize
}
}
This provides delegate methods for Extended
, for methods foo
, bar
, and baz
; the last of which needs a mutable reference to self
.
impl A for Extended {
delegate!{
inner:
quux() -> usize
}
}
This call delegates quux
and provides an implementation of the A
trait, as well as demonstrating a case where you don’t want pub
: it gets an error in a trait implementation.
Comments
Could be nice to automatically take a trait implemented for Basic and use a macro to delegate it all to Extended, not method by method.
Anonymous ‘2016-01-23T12:39:45.402-06:00’
@Anonymous: unfortunately that’s impossible! Macros operate on syntax only, no access to types or trait lookups. Possibly you could wrap a macro around the entire trait definition, but it would be a monstrosity (and wouldn’t work for remote traits).
durka ‘2016-01-23T15:58:01.720-06:00’
@durka, exactly.
Wouldn’t It Be Neat If (WIBNI*) macros could examine existing Rust code, like being able to find information about a trait’s methods? Hmm.
I assume compiler extensions for things like derive can (and do) do this already, but I haven’t even glanced at how to write those.
- A WIBNI is sort of like a RFC proposal or something; the difference is that I have no idea how to do it.
Tommy McGuire ‘2016-01-23T20:51:34.973-06:00’