A better solution to our problems with Diff
In previous sections, we've seen a few problems with writing a template that can define either a struct or an enum. We built a brute-force solution that was workable if not especially elegant. Here, we'll show a better way.
We'll start by revisiting those problems; this time we'll show a better solution to each one. After we've gone over those solutions, we'll put them together into a working template.
Revisiting our problems
So, how can we make our template?
Solving problem 1: $tdefkwd
for a top-level keyword
We needed a way to get the right keyword
for our new type.
This is available nicely as $tdefkwd
("toplevel definition keyword"),
which expands to struct
, union
, or enum
as appropriate.
Solving problem 2: $tdefgens
for generics with defaults
We needed a way to
declare generics along with their defaults.
This come in the form of $tdefgens
,
which gives the generics from the top-level type
along with their bounds
and their defaults.
Solving problem 3: $tdefvariants
to brace our variants as needed
We needed some way to wrap our enum variants in braces, but leave our structs unwrapped.
Derive-deftly solves this problem with
the $tdefvariants
expansion,
which adds braces only when the top-level type is an enum.
In other words, ${tdefvariance ARG}
expands to {ARG}
for an enum,
but ARG
otherwise.
Solving problem 4: $vdefbody
and $fdefine
for struct/variant types
We needed some way to make approprite type and field declarations no matter whether the current struct or variant is a unit struct/variant, a tuple struct/variant, or a struct with named fields/fields.
We solve this problem in two parts.
First, the $vdefbody
expansion gives us the
appropriate syntax to declare a variant or structure
corresponding to the current variant/structure,
with the necessary braces, parentheses, and/or trailing semicolon or comma.
Specifically, ${vdefbody VNAME FIELDS}
will expand to one of:
FIELDS;
for unit structs (though typically FIELDS will be empty)(FIELDS);
for tuple structs{FIELDS}
for structs with named fieldsVNAME FIELDS,
for a unit variant in an enum (though typically FIELDS will be empty)VNAME(FIELDS),
for a tuple variant in an enumVNAME { FIELDS },
for a variant with named fields in an enum.
Second,
to provide a field name and colon if appropriate, we use
${fdefine NAME}
,
which expands to NAME:
if fields are named,
and to nothing otherwise.
Solving problem 5: $fdefvis
for visibility
We needed some way to define
appropriate visibility
for struct fields,
given that pub
isn't allowed to appear in an enum definition.
For this, we use $fdefvis
,
which expands to the visibility of the current field in a struct,
and to nothing in an enum.
Putting it all together
This probably sounds like a lot, and admittedly it can be tricky to synthesize.
As a template, it's handy to start by modifying this syntax, which just reproduces the original type with a new name.
$tvis $tdefkwd $<$tname Copy><$tdefgens>
${tdefvariants $(
${vdefbody $vname $(
$fdefvis ${fdefine $fname} $ftype,
) }
) }
Based on that, we produce the following.
(As an added bonus, we'll define the actual diff
method!)
#![allow(unused)] fn main() { pub trait Diff { type Difference: std::fmt::Debug; fn diff(&self, o:&Self) -> Option<Self::Difference>; } impl Diff for u32 { type Difference = (); fn diff(&self,o:&Self) -> Option<()> {None} } use derive_deftly::{define_derive_deftly, Deftly}; define_derive_deftly!{ Diff: ${if is_union {${error "Unions aren't supported"}}} ${define DIFF_TYPE { Option< <$ftype as Diff>:: Difference> } } #[derive(Debug)] $tvis $tdefkwd $<$tname Diff><$tdefgens> ${tdefvariants $( ${vdefbody $<Both $vname> $( $fdefvis ${fdefine $fname} $DIFF_TYPE , ) } ) ${if is_enum { TypeChanged { original_value: $ttype, new_value: $ttype, }, }} } impl<$tgens> $<$ttype Diff> where $twheres { /// Helper: Return this value if it reflects a real change. fn if_is_a_change(self) -> Option<Self> { match &self { $( // (1) ${vpat self=$<$ttype Diff> vname=$<Both $vname>} => { if $( $fpatname.is_none() && ) true { return None; } } ) ${if is_enum { Self::TypeChanged{..} => {} }} } return Some(self); } } impl<$tgens> Diff for $ttype where ${if is_enum {$ttype: Clone,}} $twheres { type Difference = $<$ttype Diff>; fn diff(&self, other: &Self) -> Option<Self::Difference> { match (self, other) { $( // (2) ($vpat, ${vpat fprefix=other_}) => { // (3) ${vtype self=$<$ttype Diff> vname=$<Both $vname>} { $( $fname : Diff::diff($fpatname, $<other_ $fname>), ) } } ) // End variants. ${if is_enum { (original_value, new_value) => { Self::Difference::TypeChanged { original_value: original_value.clone(), new_value: new_value.clone(), } } }} }.if_is_a_change() } } } use derive_deftly_template_Diff; #[derive(Clone,Debug,Deftly)] #[derive_deftly(Diff)] struct Unit; #[derive(Clone,Debug,Deftly)] #[derive_deftly(Diff)] struct Tuple(u32, u32); #[derive(Clone,Debug,Deftly)] #[derive_deftly(Diff)] struct Named { a: u32, b: u32 } #[derive(Clone,Debug,Deftly)] #[derive_deftly(Diff)] enum Enum { Un, Tup(u32,u32), Nam { a: u32, b: u32 } } }
With understanding, this approach should appear much terser and more logical than the many-cases approach from the last section.
We've introduced a few previously unseen expansions: let's talk about them now.
At the beginning, we use $error
.
If it is ever expanded,
then the whole template expansion is rejected with a compile-time error.
We use it to make sure that nobody tries to apply our template
to a union
.
We use $vpat
in a new form.
As discussed in the reference,
$vpat
can take arguments to change its behavior.
We use this feature twice:
- At
// (1)
we use theself
argument to make our pattern destructure the Diff object rather than the original top-level type, and we usevname
to destructure a$<Both $vname>
variant rather than the original variant name. - At
// (2)
, we use thefprefix
argument to give the second instance of our pattern a different set of binding names, so they will not conflict with the first instance.
Finally, we pass arguments to $vtype
at // (3)
,
so that instead of constructing an instance of the current variant,
it constructs the *Diff
type with an appropriate Both*
name.