[ docs: crate top-level | overall toc, macros | template etc. reference | guide/tutorial ]
Introduction: Using derive-deftly for easy derive macros.
derive-deftly is a Rust package
that you can use to define your own derive
macros
without having to write low-level procedural macros.
The syntax is easy to learn, but powerful enough to implement macros of significant complexity.
Here's a simple example, to help you get a feel for the system.
Example: deriving field accessor functions
Suppose you want to add accessor functions
for the fields in your struct
.
You could do it by hand, like this:
#![allow(unused)] fn main() { struct MyStruct { a: Vec<u8>, b: (u32, u32), c: Option<u16>, d: (u32, u32), // (more fields here ...) } impl MyStruct { fn get_a(&self) -> &Vec<u8> { &self.a } fn get_b(&self) -> &(u32, u32) { &self.b } fn get_c(&self) -> &Option<u16> { &self.c } fn get_d(&self) -> &(u32, u32) { &self.b } // (more accessors here...) } }
But this is time consuming,
and potentially error-prone.
(Did you see the copy-paste error in get_d
?)
If you had to define a large number of accessors like this,
or do it for a bunch of types, you might want an easier way.
(Of course, in this case, you could use an existing crate. But let's suppose that there was no crate meeting your needs, and you had to build your own.)
Here's how you could define and use a derive-deftly template to save some time and risk.
#![allow(unused)] fn main() { use derive_deftly::{define_derive_deftly, Deftly}; // Defines the template define_derive_deftly! { Accessors: // Here, $ttype will expand to the toplevel type; in this case, // "MyStruct". impl $ttype { // This $( ... ) block will be repeated: in this case, once per field. $( // Here we define a "get_" function: In this definition, // $ftype is the type of the field, // $fname is the name of the field, // and $<...> denotes token pasting. fn $<get_ $fname>(&self) -> &$ftype { &self.$fname } ) } } // Applies the template to your struct #[derive(Deftly)] #[derive_deftly(Accessors)] struct MyStruct { a: Vec<u8>, b: (u32, u32), c: Option<u16>, d: (u32, u32), // (more fields here...) } }
Note 1:
This example is deliberately simplified for readability. As written here, it only works for
struct
s with no generic parameters. Later on, we'll learn how to write templates that work toenum
s, generic types, and more.
Note 2:
Some of the accessors above aren't the ones you'd write yourself in idiomatic Rust: You'd probably want to return
&str
instead of&String
, and&[u8]
instead of&Vec<u8>
.Once again, we'll be able to do this more idiomatically once we're more familiar with
derive-deftly
.
What, exactly, can derive-deftly do?
The derive-deftly
crate helps you do a lot of neat stuff:
- You can define sophisticated templates that apply,
like
derive()
macros, to structs, enums, and unions. - You can apply your templates to your own structs, enums, and unions.
- You can export your templates for use by other crates.
- Your templates can define new types, functions, methods, and variables: any kind of item that Rust allows.
- Within your templates, you can look at nearly everything about the input type: fields, variants, attributes, types, and so on.
- Your templates can use complex control structure to define configurable behavior or special cases.
Still, there are a number of things that derive-deftly cannot do, or cannot do yet:
- You can't apply a
derive-deftly
template to anything besides a struct, enum, or union. For example, you can't apply it to afn
declaration or animpl
block. - Like a
derive
macro, aderive-deftly
template cannot change the type it's applied to: You can define a new type, but you can't change the definition of the original type. - The
derive-deftly
template language, though powerful, does not attempt to be a general-purpose programming language. There will always be some problems better solved through procedural macros, or through changes to the Rust language. - Because of limitations in the Rust macro system,
derive-deftly
templates have to be applied using the syntax above. (That is, in the example above, you need to say#[derive(Deftly)]
and#[derive_deftly(Accessors)]
. You can't define a macro that delivers#[derive(Accessors)]
directly.
About this book
In the rest of this book, we'll explain how to use derive-deftly in detail. We'll try to explain each of its features, and how to use it to make correct, reliable templates that handle a broad range of situations.
Other resources
There are other resources on derive-deftly:
- The Reference tries to provide is a terse but complete (and farily rigorous) guide to derive-deftly's behavior and features.
- The rustdoc describes the exposed members of the
derive-deftly
crate, along with other useful information.
[ docs: crate top-level | overall toc, macros | template etc. reference | guide/tutorial ]
Getting started
In this short chapter,
we'll talk about a few things you should know
before you start using derive-deftly
,
and we'll show you a simple "hello world" example.
What you should know before you start
Before you start using derive-deftly
,
there are a few things t2hat this guide assumes
you are already familiar with.
Don't feel bad if you don't know this stuff already;
just make a note to come back later
once you're ready.
-
We assume that you are already familiar with the Rust programming language and the Cargo package management tool. If you're not, maybe try the Rust Book or Rust By Example.
-
We assume that you are comfortable writing, building, compiling, and debugging Rust programs.
-
We assume that you are already familiar with the
macro_rules!
syntax for defining "macros by example". If you're not, see the relevant sections in the above books.
If these assumptions are true for you, then let's move ahead!
A first example: derive_deftly(HelloWorld)
Here we'll make an example project
with a simple derive-deftly
template.
Our template won't do much,
but it will get us started using the system.
To begin with, we'll create a new project:
$ cargo init --lib dd-hello-world $ cd dd-hello-world
Next, we'll add the derive-deftly
crate as a new dependency:
Note: You could also add the dependency by adding a line like this to the
[dependencies]
section in yourCargo.toml
:derive-deftly = "0.10"
(You should replace
0.10
with the real latest version.)
Now we can edit src/lib.rs
in our project
to define and use a new template.
Writing our HelloWorld template
There are two parts to using derive-deftly: specifying templates that you can use to derive new features for your structs and enums and then applying those templates to your types.
To define a template, you use
define_derive_deftly!
, as in
#![allow(unused)] fn main() { use derive_deftly::define_derive_deftly; define_derive_deftly! { HelloWorld: impl $ttype { pub fn greet() { println!("Greetings from {}", stringify!($ttype)); } } } }
This is a very simple template: it uses a single expansion: $ttype
.
(We'll get into more expansions, and more useful examples, later on.
For now, all you need to know
is that $ttype
expands to the type
on which you're applying your template.)
Later on, you can apply HelloWorld
to your own type
with [#[derive(Deftly)]
derive-Deftly, as in:
#![allow(unused)] fn main() { use derive_deftly::define_derive_deftly; define_derive_deftly! { HelloWorld: impl $ttype { pub fn greet() { println!("Greetings from {}", stringify!($ttype)); } } } use derive_deftly::Deftly; #[derive(Clone, Debug, Deftly)] #[derive_deftly(HelloWorld)] pub struct MyStruct; MyStruct::greet(); }
Limitations of our template
Our template won't work for every type! For example, suppose that we try to apply it to a generic type:
#[derive(Deftly)]
#[derive_deftly(HelloWorld)]
struct Pair<T> {
first: T,
second: T,
}
In response, derive-deftly will try to expand our template to something like this:
impl Pair {
pub fn greet() {
println!("Greetings from {}", stringify!(Pair));
}
}
But that won't work in Rust. Instead, we would need a template to expand to something more like this:
impl<T> Pair<T> {
...
}
We'll talk about how to make templates work for types like this in later chapters.
What's next?
Next, we'll take a detour, and show how to expose our template so that it can be used in other modules, and other crates.
After that,
we'll look over a more complex example,
and learn more features of derive-deftly
.
We'll write a more useful template,
and have it apply to more types.
Using templates from other crates and modules
In the previous section,
we learned how to crate our first template.
We used derive-deftly
to define a template called HelloWorld
that could print the name of a type.
Before we go any further, we'll discuss how to use that template from other modules within the same crate, and how to expose that template so that other crates can use it.
We'll also discuss a convenient way to define a template that only needs to be used once.
Templates inside modules
When you define a template, derive-deftly turns it into a definition
of a macro_rules!
macro.
In the example above, that would be derive_deftly_template_HelloWorld
.
Unfortunately,
Rust's rules for scoping of macro_rules!
macros
are awkward and confusing.
In general, there are two ways to expose a macro;
we'll go over how to use each one
for a derive-deftly template.
With path-scoped macros
With modern versions of Rust
(Rust 2018 and later),
you can use
path-based scope to define the scope of a macro.
With this method, you use
pub use
(or pub(crate) use
, etc)
to define the scope for a macro,
and allow it to be accessed by its path.
pub mod hello_world { use derive_deftly::define_derive_deftly; define_derive_deftly! { HelloWorld: /* ... */ } pub(crate) use derive_deftly_template_HelloWorld; } mod caller { use derive_deftly::Deftly; use super::hello_world::derive_deftly_template_HelloWorld; #[derive(Deftly)] #[derive_deftly(HelloWorld)] pub struct MyStruct; // Alternatively, you use the path, and avoid having to say "use": #[derive(Deftly)] #[derive_deftly(super::hello_world::HelloWorld)] pub struct MyOtherStruct; } fn main() {} // so that the above is not wrapped in fn main().
This is generally the better method to use.
Using #[macro_use]
We can also expose a macro using textual scope and macro_use
:
We prefix
that module's mod
statement with #[macro_use]
.
If you do this, the module defining the macro
must come before the module where you use the macro.
(Also, the macro's name is never scoped within the module.)
#![allow(unused)] fn main() { #[macro_use] // make HelloWorld visible outside the module (within the crate) mod hello_world { use derive_deftly::define_derive_deftly; define_derive_deftly! { HelloWorld: /* ... */ } } mod caller { // must come after mod hello_world use derive_deftly::Deftly; #[derive(Deftly)] #[derive_deftly(HelloWorld)] // not hello_world::HelloWorld pub struct MyStruct; } }
Exporting templates
But now suppose that you want to expose your template, so that people can use it from any crate.
To do this, you use
export
before the name of your macro:
pub trait HelloWorld { fn greet(); } derive_deftly::define_derive_deftly! { /// Derives `HelloWorld`, providing `greet` export HelloWorld: impl $crate::HelloWorld for $ttype { fn greet() { println!("Greetings from {}", stringify!($ttype)); } } } fn main() {}
Declaring our template as
export HelloWorld
in this way is equivalent to markingderive_deftly_template_HelloWorld
with#[macro_export]
. Doing this puts the template into the root of the crate. You can make the template visible elsewhere usingpub use
.
Note that this time,
we've defined HelloWorld
as a trait,
and we've changed our template to refer to that trait as
$crate::HelloWorld
.
The
$crate
syntax will expand to the name of the crate
in which our template was defined,
so that when later we expand this template in a different crate,
it will find the right trait.
(Otherwise, it would fail if the HelloWorld
trait were not in scope.)
We've also added a doc comment, which will appear in the public API documentation for our crate.
Additionally,
we need to re-export derive_deftly
from our crate, so that when our template is applied to user types,
a compatible derive-deftly driver is used.
And we should also invoke
derive_deftly::template_export_semver_check
once,
somewhere in our crate:
this will tells us about any implications for your crate's semver,
when you change your Cargo.toml
to upgrade the derive-deftly
crate.
// At the root of our crate:
#[doc(hidden)]
pub use derive_deftly;
// Use the current version of derive_deftly here:
derive_deftly::template_export_semver_check!("0.11.0");
Now, when somebody wants to use our template from a different crate, they can do it like this:
// Let's pretend our crate is called hello_world.
use hello_world::{
// This is the trait we defined...
HelloWorld,
// This is the macro that makes our template work.
// (We might come up with a better syntax for this later).
derive_deftly_template_HelloWorld
};
use derive_deftly::Deftly;
#[derive(Deftly)]
#[derive_deftly(HelloWorld)]
struct TheirStructure {
// ...
}
Note that exporting a template to other crates with export
doesn't affect its visibility within your crate.
You may still need #[macro_use]
.
What's next
Now that we've explained how to expose a template,
it's time to learn about more features from derive-deftly.
In the next session, we'll start working on a Clone
example,
and we'll learn how to work with
structs, enums, fields, variants, and more.
If you're only deriving once...
If you want, you can apply a template to an existing type without having to name that template. You might want to do this if you have a template that you only want to apply to a single struct, and so you don't want to bother naming it.
Supposing that you wanted to apply the template above to MyStruct
and MyStruct
alone,
you could have said:
#![allow(unused)] fn main() { use derive_deftly::{Deftly, derive_deftly_adhoc}; #[derive(Clone, Debug, Deftly)] #[derive_deftly_adhoc] pub struct MyStruct; derive_deftly_adhoc!{ MyStruct: impl $ttype { pub fn print_name() { println!("The name of this type is {}", stringify!($ttype)); } } } }
Of course, that's not so useful yet.
In this case, it would have been easier just to write
impl MyStruct { pub fn print_name() { ... } }
.
But soon, we'll see how to write more interesting templates,
and how to use them to create much more interesting code.
The rest of this document will focus on how to use derive_deftly's template features to your fullest advantage. If you're the kind of person who wants to skip straight to the reference manual, you can find it over here.
Example 1: Writing your own Clone
In the previous chapters,
we've learned how to define a simple derive-deftly
template,
and how to export it to the rest of the world.
In this chapter, we'll use derive-deftly
to implement our own version of Rust's standard
#[derive(Clone)]
macro.
At first, it will be very simple;
later on, we'll add a few features
that Rust's derive(Clone)
doesn't have.
With these examples, we'll learn more feature of derive-deftly, including:
- Writing templates that apply to structs and enums.
- Writing templates that apply to generic types.
- Using a variety of expansions, including ones for top-level types, variants, and fields.
Aside:
We've picked a simple trait to derive on purpose, so that we can focus on the features of derive-deftly without the additional complexity of introducing an unfamiliar trait as well.
Please let us know if this approach works for you! We're learning how to explain these concepts as we go along.
Working with structs and fields
Let's start by imagining that we had to write Clone from scratch for a simple structure like this:
#![allow(unused)] fn main() { struct GiftBasket { n_apples: u8, n_oranges: u8, given_from: Option<String>, given_to: String, } }
We'd probably write something like this:
#![allow(unused)] fn main() { struct GiftBasket { n_apples: u8, n_oranges: u8, given_from: Option<String>, given_to: String, } impl Clone for GiftBasket { fn clone(&self) -> Self { Self { n_apples: self.n_apples.clone(), n_oranges: self.n_oranges.clone(), given_from: self.given_from.clone(), given_to: self.given_to.clone() } } } }
(In reality,
since n_apples
and n_oranges
both implement Copy
,
you wouldn't actually call clone()
on them.
But the compiler
should be smart enough to
optimize the clone()
away.)
If you imagine generalizing this to any simple struct with named fields, you might come up with a pseudocode template like this one:
impl Clone for ⟪Your struct⟫ {
fn clone(&self) -> Self {
Self {
for each field:
⟪field name⟫: self.⟪field name⟫.clone(),
}
}
}
And here's how that pseudocode translates into a derive-deftly template:
#![allow(unused)] fn main() { use derive_deftly::define_derive_deftly; define_derive_deftly! { MyClone: impl Clone for $ttype { fn clone(&self) -> Self { Self { $( $fname : self.$fname.clone() , ) } } } } }
Let's look at that template piece by piece.
Certain marked "expansion keywords" in the contents
(those starting with $
)
are replaced with a value that depends on the struct
we are applying the template to.
You've already seen $ttype
("top-level type"):
it expands
to the type on which you are applying the macro. There are two new
pieces of syntax here, though:
$( ... )
and
$fname
.
In derive-deftly templates, $( ... )
denotes repetition:
it repeats what is inside it
an "appropriate" number of times.
(We'll give a definition of "appropriate" later on.)
Since we want to clone every field in our struct,
we are repating the field: self.field.clone() ,
part of our implementation
once for each field.
The $fname
("field name") expansion means "the name of the current field".
Which field is that?
Since $fname
occurs inside $( ... )
,
we will repeat the body of the $( ... )
once for each
field, and expand $fname
to the name of a different field each time.
(Again, more advanced repetition is possible; there's more to come.)
An aside on naming
At this point, you've seen expansions with names like $ttype
and $fname
, and you may be wondering where these names come from.
If you want to know more,
you can skip ahead to the section
on naming conventions.
Will MyClone apply to tuple and unit structs?
Rust defines several kinds of struct:
- structs with fields like
struct Foo {...};
- tuple structs like
struct Foo(...);
- and unit structs like
struct Foo;
So far, you've only applied your MyClone
template
to a struct with fields.
But with a tuple struct, or a unit struct,
you might expect it to fail.
Surprisingly, our template still works fine!
This isn't because of any clever trickery
from derive-deftly:
it's just how Rust works.
When you use MyClone
on tuple or unit struct,
it will expand to code like this the example below.
And this syntax,
though not the normal way to work with these types,
is still valid Rust!
#![allow(unused)] fn main() { struct TupleStruct(String, Option<String>); impl Clone for TupleStruct { fn clone(&self) -> Self { Self { 0: self.0.clone(), 1: self.1.clone(), } } } struct UnitStruct; impl Clone for UnitStruct { fn clone(&self) -> Self { Self { } } } }
This will be a common theme when using derive-deftly: Rust often lets you use a slightly unidiomatic syntax, so that you can handle many different cases in the same way.
Making MyClone apply to enumerations
At this point, you've probably noticed
that we've defined MyClone
to apply to struct
s only,
but it won't (yet) work on enum
s.
Let's fix that!
Suppose that we have enumeration defined like this:
#![allow(unused)] fn main() { enum AllTypes { NoData, Tuple(u8, u16), Struct { a: String, b: String } } }
We want to make sure that MyClone can recognize and re-construct each of the three variants.
We can do that as follows:
#![allow(unused)] fn main() { use derive_deftly::define_derive_deftly; define_derive_deftly! { MyClone: impl Clone for $ttype { fn clone(&self) -> Self { match self { $( $vpat => $vtype { $( $fname: $fpatname.clone(), ) }, ) } } } } }
Note that now we have two levels of nested repetition.
First, we match once for each variant.
(This is at the $vpat
and $vtype
level.)
Then we match once for each field of each variant.
(This is at the $fname
and $fpatname
level.)
Let's go over the new expansions here.
First, we have
$vpat
("variant pattern"):
that expands to a pattern that can match and deconstruct
a single variant.
Then, we have
$vtype
("variant type"):
that's the type of the variant,
suitable for use as a constructor.
Then, inside the variant, we have $fname
:
that's our field name, which we've seen it before.
Finally, we have
$fpatname
("field pattern name"):
that is the name of the variable that we used for this field
in the pattern that deconstructed it.
When we apply MyClone
to our enumeration,
we get something like this:
#![allow(unused)] fn main() { enum AllTypes { NoData, Tuple(u8, u16), Struct { a: String, b: String } } impl Clone for AllTypes { fn clone(&self) -> Self { match self { AllTypes::NoData {} => AllTypes::NoData {}, AllTypes::Tuple { 0: f_0, 1: f_1, } => AllTypes::Tuple { 0: f_0.clone(), 1: f_1.clone() }, AllTypes::Struct { a: f_a, b: f_b, } => AllTypes::Struct { a: f_a.clone(), b: f_b.clone() }, } } } }
Note that our template above will still work fine on a regular struct,
even though it's written for an enum
.
If we apply the version of MyClone
above
to struct Example { a: u8, b: String }
,
we get this:
#![allow(unused)] fn main() { struct Example { a: u8, b: String } impl Clone for Example { fn clone(&self) -> Self { match self { Example { a: f_a, b: f_b, } => Example { a: f_a.clone(), b: f_b.clone(), } } } } }
So (in this case at least)
we were able to write a single template expansion
that worked for both struct
s and enum`s.
Making MyClone apply to generics
We've gotten a MyClone
implementation
that works with several kinds of struct struct, and with enums too.
But here's a structure where our current MyClone
implementation
will fall flat:
#![allow(unused)] fn main() { use std::fmt::Debug; struct MyItems<T:Clone, U> where U: Clone + Debug { things: Vec<T>, items: Vec<U> } }
When we go to expand our template, it will generate something like:
impl Clone for MyItems { ... }
That isn't valid! For our expansion to compile, we would need to use the generic parameters and their "where" constraints, like so:
impl<T:Clone, U> Clone for MyItems<T,U>
where U: Clone+Debug
{ ... }
To do this with derive-deftly
, we have to expand our MyClone
templatate as shown below:
Therefore, to get our MyClone
template working with generic types,
we can expand it to add the same parameters and constraints.
#![allow(unused)] fn main() { use derive_deftly::{define_derive_deftly,Deftly}; define_derive_deftly! { MyClone: // (The next three lines are new) impl<$tgens> Clone for $ttype where $twheres { // (The rest is as before...) fn clone(&self) -> Self { match self { $( $vpat => $vtype { $( $fname: $fpatname.clone(), ) }, ) } } } } use std::fmt::Debug; #[derive(Deftly)] #[derive_deftly(MyClone)] struct MyItems<T:Clone, U> where U: Clone + Debug { things: Vec<T>, items: Vec<U> } #[derive(Deftly)] #[derive_deftly(MyClone)] enum TestCase<A: Clone> { Variant(A) } }
Here we meet two new keywords.
$tgens
("top-level generics") becomes
the generic parameters as declared on the top-level type.
(In our case, that's T:Clone, U
.)
The $twheres
keyword
("top-level where clauses") becomes
the where
constraints as declared on the top-level type.
(In our case, that's U: Clone+Debug
.)
Note that $ttype
expands to the top-level type:
that's now MyItems<T,U>
,
which is what we want in this case.
If we had wanted only MyItems
without <T,U>
,
we would say $tname
instead.
But does the syntax work for non-parameterized types?
Will this template still work for non-parameterized types? Indeed, it will! To Rust, this syntax is perfectly fine:
#![allow(unused)] fn main() { struct Simple { a: String } // Note empty <> and "where" list: impl<> Clone for Simple where { fn clone(&self) -> Self { Self { a: self.a.clone(), } } } }
Making MyClone apply conditionally
Now, for the first time, we will make MyClone
do something
that Rust's #[derive(Clone)]
does not:
it will apply only when the fields of a struct are Clone
.
For example, suppose have a struct like this:
#![allow(unused)] fn main() { use std::sync::Arc; struct Indirect<T>(Arc<T>, u16); }
If you try to derive Clone
on it,
the compiler will generate code something like this:
impl<T: Clone> Clone for Indirect<T> { ... }
But that T: Clone
constraint isn't strictly necessary: Arc<T>
always
implements Clone
, so your struct could have been be Clone
unconditionally.
But using derive-deftly,
you can define a template
that derives Clone
only for the cases
where the actual required constraints are met:
#![allow(unused)] fn main() { use derive_deftly::{define_derive_deftly,Deftly}; define_derive_deftly! { MyClone: impl<$tgens> Clone for $ttype where $twheres // This is the new part: $( $ftype : Clone , ) { // (The rest is as before...) fn clone(&self) -> Self { match self { $( $vpat => $vtype { $( $fname: $fpatname.clone(), ) }, ) } } } } use std::{sync::Arc, fmt::Debug} ; #[derive(Deftly)] #[derive_deftly(MyClone[dbg])] struct Example<T> where T: Debug { arc: Arc<T> } #[derive(Deftly)] #[derive_deftly(MyClone[dbg])] struct Example2<T> { arc: Arc<T> } }
Here, we are using
$ftype
.
("field type") to get the actual type of each field.
Since we're repeating it with $( ... )
,
we are requiring every field to be Clone
.
Will this work with non-generic fields, or if the same field is used more than once? Once again, yes! To Rust, this is a perfectly valid example:
impl<T> Clone for Direct
where
T: Clone,
T: Clone,
String: Clone
{
...
}
What about that comma?
If you're paying close attention, you might have thought
we had a syntax error above
when we didn't use an explicit comma
after where $twheres
.
This time,
derive_deftly
has exactly one piece of cleverness at work.
It makes sure that either $twheres
is empty,
or that it ends with a comma.
That way, when your template expands
where $twheres $( $ftype : Clone , )
it won't produce
where U: Debug + Clone T: Clone(which is a syntax error) or
where ,`
(which is also a syntax error).
A further note on repetition
Note that when we define our additional where
clauses above, we said
where $( $ftype: Clone, )
at the top level.
We didn't have to specify separate of repetition
for variants and fields:
if we have only $ftype
in a top-level repetition,
derive_deftly
will iterate over all fields in all variants.
Sometimes, if you do something subtle,
derive-deftly may not be able to figure
out what you're trying to repeat over.
You can use
${for fields {...}}
or ${for variants {...}}
to specify explicitly what you want to repeat.
So, above, instead, we could have written
#![allow(unused)] fn main() { use derive_deftly::{Deftly, derive_deftly_adhoc}; #[derive(Deftly)] #[derive_deftly_adhoc] enum TestCase<A> { Variant(A) } derive_deftly_adhoc! { TestCase: fn testcase<$tgens>() where ${for fields { $ftype: Clone, }} {} } }
You can also use ${for ...}
rather than $(...)
in cases where you feel
it makes your macro code clearer.
For example, we could have used ${for}
to write our MyClone
example more explicitly like this:
#![allow(unused)] fn main() { use derive_deftly::define_derive_deftly; define_derive_deftly! { MyClone: impl<$tgens> Clone for $ttype where $twheres ${for fields { $ftype: Clone , }} { fn clone(&self) -> Self { match self { ${for variants { $vpat => $vtype { ${for fields { $fname: $fpatname.clone(), }} }, }} } } } } }
Some more advanced topics
Now that we've our first basic example under our belt,
let's look at some other things that derive-deftly
can do.
Transforming names and strings
Often, it's useful to define new identifiers based on existing ones, or to convert identifiers into strings.
You could use the existing paste
crate for this,
or you can use a native facility provided by derive-deftly.
For example, suppose that you want
to define a template that makes a "discriminant" type
for your enumerations.
You want the new type to be named FooDiscriminant
,
where Foo
is the name of your existing type.
While you're at it, you want to add an is_
function
to detect each variant.
You can do that like this:
#![allow(unused)] fn main() { use derive_deftly::define_derive_deftly; define_derive_deftly! { Discriminant: #[derive(Copy,Clone,Eq,PartialEq,Debug)] enum ${paste $tname Discriminant} { $( $vname, ) } impl<$tgens> $ttype where $twheres { fn discriminant(&self) -> ${paste $tname Discriminant} { match self { $( $vpat => ${paste $tname Discriminant}::$vname, ) } } $( fn ${paste is_ ${snake_case $vname}} (&self) -> bool { self.discriminant() == ${paste $tname Discriminant} ::$vname } ) } } }
Here we see a couple of new constructs.
First, we're using
${paste}
to glue several identifiers together into one.
When we say ${paste $tname Discriminant}
,
we are generating a new identifier from $tname
(the type name)
and the word Discriminant.
So if the type name is Foo
,
the new type will be called FooDiscriminant
.
Second, we're using
${snake_case}
to transform an identifier into snake_case
(that is, lowercase words separated by underscores).
We use this to turn the name of each variant ($vname
)
into a name suitable for use in a function name.
So if a variant is called ExampleVariant
,
${snake_case $vname}
will be example_variant
,
and ${paste is_ ${snake_case $vname}}
will be
is_example_variant
.
There are other case-changers:
${pascal_case my_ident}
becomesMyIdent
. You can also write this as${upper_camel_case ..}
.${lower_camel_case my_ident}
becomesmyIdent
.${shouty_snake_case MyIdent}
becomesMY_IDENT
.${snake_case MyIdent}
becomesmy_ident
, as you've already seen.
You can abbreviate ${paste ...}
as $<...>
.
Pasting onto types
Here's a side-note:
You can use ${paste}
to append identifiers to a type
(like $ttype
or $ftype
),
not just an identifier.
When you do this, ${paste}
will
do the right thing
even if the type is generic.
For example, if $ftype
is Vec<u8>
,
then ${paste $ftype Builder}
will expand to
VecBuilder<u8>
, not Vec<u8>Builder
.
A note on syntax
In this last section,
you've seen a new syntax for the first time.
Both ${paste ident ident..}
and ${snake_case ident}
are special cases of the following meta-syntax,
which derive-deftly uses everywhere:
In fact, if you want,
you can use this format
for all of the expansion macros you have already seen:
$ttype
is just a shortened form for ${ttype}
,
$fname
is just ${fname}
,
and so on.
Some keywords, including some of those we've already seen, can take named arguments. The syntax for this is:
${KEYWORD ARGNAME=VALUE ARGNAME=VALUE...}
For example, we can use this syntax to give optional arguments to
$vpat
; see the template syntax reference for more information.
If you ever need to write a literal $
(say, if you want to confuse yourself
by making derive-deftly-generated pattern macros)
you can write $$
.
Naming conventions for derive-deftly expansions
Here we discuss some naming conventions used in the naming of derive-deftly expansions.
Many derive-deftly expansions' names start with a single letter to indicate what information they use:
t
for top-level (thestruct
,enum
, orunion
you are applying the template to),v
for variant (a variant of anenum
),- or
f
for field (a single field of a struct or variant).
For example, there is $ttype
for "top-level type" and $fname
for
"field name".
(We say "top-level" instead of "struct", since the "top-level" part of a declaration can also be an enum or a union.)
Many derive-deftly expansions end with
a short identifier for what they contain.
For example, $tname
is the name of a top-level type,
$vname
is the name of a variant,
and $fname
is the name of a field.
Whenever possible (and logical), we have tried to use the same
identifier for the t
, v
, and f
cases.
Example 2: Defining a constructor function
In this section,
we'll be using another example to demonstrate
more of what derive-deftly
can do.
We'll be building a Constructor
template
to define a new()
function for a struct,
without having to write out all of its arguments.
Let's start with the following (struct-only) template:
#![allow(unused)] fn main() { use derive_deftly::define_derive_deftly; define_derive_deftly! { Constructor: impl<$tgens> $ttype where $twheres { pub fn new( $( $fname: $ftype , ) ) -> Self { Self { $( $fname , ) } } } } }
When you apply the above template to a type like this:
#![allow(unused)] fn main() { use derive_deftly::define_derive_deftly; define_derive_deftly! { Constructor: impl<$tgens> $ttype where $twheres { pub fn new( $( $fname: $ftype , ) ) -> Self { Self { $( $fname , ) } } } } use derive_deftly::Deftly; #[derive(Deftly)] #[derive_deftly(Constructor)] struct Ex<A> { a: f64, b: A, c: String } }
You'll get a constructor like this:
#![allow(unused)] fn main() { struct Ex<A> { a: f64, b: A, c: String } impl<A> Ex<A> { pub fn new( a: f64, b: A, c: String ) -> Self { Self { a, b, c } } } }
So far, there aren't any new techniques at work here. We'll add some more down below.
Limiting a template to one kind of type.
The Constructor
template above doesn't work for enumerations.
If you try to apply it to one, you'll get
a not-entirely-helpful error message.
In earlier examples, we've shown how to make templates that apply to enums as well as structs. But let's suppose that in this case, we want our template to be struct-only.
We can tell derive-deftly about this restriction, to help it generate more useful error messages:
#![allow(unused)] fn main() { use derive_deftly::define_derive_deftly; define_derive_deftly! { Constructor for struct: // (1) // The rest is unchanged impl<$tgens> $ttype where $twheres { pub fn new( $( $fname: $ftype , ) ) -> Self { Self { $( $fname , ) } } } } }
(Note the use of
for struct
above at // (1)
.)
Now if we try to apply our template to an enum, we'll get a more useful error:
error: template defined for struct, but applied to enum
Debugging templates
When writing complex templates,
it can sometimes be hard to figure out compiler errors.
derive-deftly
has some features which can help:
Syntax checking the expansion
You can use the expect
option
to tell derive-deftly what your macro is supposed to produce:
#![allow(unused)] fn main() { use derive_deftly::{Deftly, define_derive_deftly}; define_derive_deftly! { Constructor for struct, expect items: impl<$tgens> $ttype where $twheres { // ... } } #[derive(Deftly)] #[derive_deftly(Constructor)] struct Test; }
If the expansion fails to syntax check, you'll get not only an error pointing at the part of the template or structure which seems wrong, but also error messages pointing into a copy of the actual expansion. Hopefully you'll be able to see what's wrong there.
If your macro is supposed to expand to an expression,
you can write expect expr
instead of expect items
.
Alternatively, you can request this syntax check when you invoke the macro:
#![allow(unused)] fn main() { use derive_deftly::{Deftly, define_derive_deftly}; define_derive_deftly! { Constructor: } #[derive(Deftly)] #[derive_deftly(Constructor[expect items])] struct MyStruct { /* ... */ } }
Note: The above section isn't really logical: "expect items" is always the case for templates defined with
define_derive_deftly
: only for adhoc templates isexpect expr
reasonable.TODO: Rewrite and move this section.
Seeing a copy of the expansion
Sometimes, the expansion is syntactically valid, but is wrong in some other way.
You can use the dbg
option to tell derive-deftly to
print a copy of the expansion.
#![allow(unused)] fn main() { use derive_deftly::{Deftly, define_derive_deftly}; define_derive_deftly! { Constructor: } #[derive(Deftly)] #[derive_deftly(Constructor[dbg])] struct MyStruct { /* ... */ } }
You'll see the compiler print something like this:
---------- derive-deftly expansion of Constructor for MyStruct (start) ----------
impl<> MyStruct where {
pub fn new(...) { ... }
}
---------- derive-deftly expansion of Constructor for MyStruct (end) ----------
Just as with the dbg!
macro, you don't want to
leave this in your production code.
Working with visibility
Our Constructor
template above doesn't really make sense
if it's applied to a non-public type;
It would define the new()
function as public,
when the type is not meant to be exposed outside the crate.
(Rust may even complain that we're declaring
a public function that returns a private type!)
Let's fix this, and have our template give our constructor the same visibility as the type itself:
#![allow(unused)] fn main() { use derive_deftly::define_derive_deftly; define_derive_deftly! { Constructor for struct: impl<$tgens> $ttype where $twheres { // (this "$tvis" is new) $tvis fn new( $( $fname: $ftype , ) ) -> Self { Self { $( $fname , ) } } } } }
Here instead of saying pub fn new
,
we said $tvis fn new
.
The
$tvis
keyword will expand
to the visibility of the top-level type.
There is a similar keyword
$fvis
that expands to the visibility of the current field.
(Since enum variants are always visible,
there is no such keyword as $vvis
.
Since enum fields are always visible,
$fvis
in an enum always expands to pub
.)
Using attributes to make a template take arguments
Let's suppose we want to make our Constructor
template
a little more flexible:
we'd like to be able to give the new
function a different name.
The usual way to pass an option to a derive macro is with an attribute, something like this:
#[derive(Deftly)]
#[derive_deftly(Constructor)]
#[deftly(constructor(newfn="function_name"))]
struct Example(...);
We can extend our template to take a function name, like this:
#![allow(unused)] fn main() { use derive_deftly::define_derive_deftly; define_derive_deftly! { Constructor for struct: impl<$tgens> $ttype where $twheres { $tvis fn ${tmeta(constructor(newfn)) as ident} // (1) ( $( $fname: $ftype , ) ) -> Self { Self { $( $fname , ) } } } } use derive_deftly::Deftly; #[derive(Deftly)] #[derive_deftly(Constructor)] #[deftly(constructor(newfn="construct_example"))] struct Example { a: f64, b: String } }
Here, instead of specifying "new
"
for the method name in our template,
we give the name as ${tmeta(constructor(newfn))} as ident
.
This tells the template to look for an
#[deftly(constructor(newfn="..."))]
attribute on the type,
to interpret that attribute as an identifier,
and to use the value of that attribute
in place of the keyword.
The as ident
portion is mandatory;
without it, derive-deftly can't tell
how to parse the argument to the argument.
Instead of ident
, you can describe other kinds syntactical types,
such as str
, ty
, path
, or expr
.
See the reference for a complete list.
Note that we explicitly
named our attribute as constructor(newfn)
.
We could instead have specified it as newfn
,
but that would have the potential to conflict
with attributes interpreted by other templates.
As a best practice, if
we expect our template to be widely used,
we should namespace our attributes as in the example above.
The
$tmeta
("toplevel meta")
keyword that we used here
tells the template
to look at the #[deftly]
attributes for the type.
We can, instead, use
$vmeta
("variant meta")
to look for #[deftly]
attributes for the current variant,
or
$fmeta
("field meta")
to
to look for #[deftly]
attributes for the current field.
(We'll see an example of using $fmeta
in a little while.)
Getting started with if/then/else conditionals
In the example above,
we made it possible to rename the "new" function
generated by our template.
But our approach is flawed:
if the user doesn't provide
the #[deftly(newfn)]
attribute,
the template won't make a function name at all!
Let's show how to fix that:
#![allow(unused)] fn main() { use derive_deftly::define_derive_deftly; define_derive_deftly! { Constructor for struct: impl<$tgens> $ttype where $twheres { $tvis fn // (1) This "if" defines the function name: ${if tmeta(constructor(newfn)) { ${tmeta(constructor(newfn)) as ident} } else { new } } // The rest is as before: ( $( $fname: $ftype , ) ) -> Self { Self { $( $fname , ) } } } } use derive_deftly::Deftly; #[derive(Deftly)] #[derive_deftly(Constructor)] #[deftly(constructor(newfn="construct_example"))] struct Example { a: f64, b: String } #[derive(Deftly)] #[derive_deftly(Constructor)] struct Example2 { a: f64, b: String } }
Have a look at the part of the template marked with // (1)
.
It introduces a new concept: conditional expansion.
The
${if cond {expansion}}
keyword checks whether a given condition is true.
If it is, then the $if
keyword expands to the expansion second argument.
Otherwise, it expands to an "else" argument (if any).
Also, you can chain
$if
s, as in${if COND1 { ... } else if COND2 { ... } else { ... }
and so on!
Here, the condition is
tmeta(constructor(newfn))
("toplevel metavalue").
That condition is true if the current type
has an #[deftly(newfn)]
attribute,
and false otherwise.
There are also vmeta
and fmeta
conditions
to detect #[deftly(..)]
attributes
on variants and fields respectively.
Note: Don't confuse conditions with expansions! As we use it here,
tmeta(x)
is a condition (true or false), whereas${tmeta(x) as ident}
is an expansion.Even though the names are similar in this case, conditions and expansions have separate namespaces; you cannot (in general) use them interchangeably.
There is a complete list of recognized conditions in the reference.
Multiple exclusive options with $select1
Sometimes you want to make sure exactly one condition matches.
This is a good choice when you have multiple options for controlling some feature, and you want to make sure that the user has specified exactly one (or maybe, no more than one).
You can do this using the ${select1}
keyword:
${select1 COND1 { ... } else if COND2 { ... } ... else { ... }
Its syntax is nearly the same as ${if}
,
except that all conditions are checked.
If more than one condition is true,
then derive-deftly rejects ${select1}
:
it fails to expand, and
emits an error.
If no conditions are true,
and there is no else
clause,
then ${select1}
is also rejected
(it fails to expand,
and emits an error).
Here's an example of how we could use ${select1}
if we wanted to allow constructor(default_name)
,
as a synonym for our default behavior.
In this case, we enforce that the user has specified
constructor(newfn(X))
or constructor(default_name)
or neither,
but not both:
#![allow(unused)] fn main() { use derive_deftly::define_derive_deftly; define_derive_deftly! { Constructor for struct: impl<$tgens> $ttype where $twheres { pub fn ${select1 tmeta(constructor(newfn)) { ${tmeta(constructor(newfn))} } else if tmeta(constructor(default_name)) { new } else { new } } () {} } } }
Making Constructor set fields with Default
Sometimes, we'd like to make a template template
that behave in different ways for different fields.
For example, let's suppose that we want our Constructor
template
to be able to set fields to their default values,
and not take them as arguments.
We can do this with an explicit conditional for each field:
#![allow(unused)] fn main() { use derive_deftly::define_derive_deftly; define_derive_deftly! { Constructor: impl<$tgens> $ttype where $twheres { $tvis fn // This is the function name, same as before. ${if tmeta(constructor(newfn)) { ${tmeta(constructor(newfn)) as ident} } else { new } } ( // These are the function arguments: $( ${when not(fmeta(constructor(default))) } // (1) $fname: $ftype , ) ) -> Self { Self { $( $fname: ${if fmeta(constructor(default)) { ::std::default::Default::default() // (2) } else { $fname } } , ) } } } } use derive_deftly::Deftly; #[derive(Deftly)] #[derive_deftly(Constructor)] struct Foo { #[deftly(constructor(default))] s: Vec<String>, n: u32, } }
Here we're using a new construct:
$when
.
It's only valid inside a loop like $( ... )
or ${for ...}
.
It causes the output of the loop to be suppressed
whenever the condition is not true.
The condition in this cases is not(fmeta(constructor(default)))
.
(See // (1)
.)
You've seen fmeta
before;
it's true when a given attribute is present on the current field.
The not
condition
is just how we express negation.
All together, this $when
keyword causes each field
that has #[deftly(Constructor(default))]
applied to it
to be omitted from the list of arguments
to the new()
function.
Note at
// (2)
that we're using::std::default::Default::default()
, rather than callingDefault::default()
unconditionally. Remember, macros expand at the position where they are invoked, and it's possible that we'll be invoked in a module that has been built without the standard prelude.
Besides $not
,
you can use other boolean operators in conditions too:
there is an
any(...)
that is true
whenever at least one of its arguments is true,
and an
all(...)
that is true
when all of its arguments are true.
Managing complexity with $define
Our constructor example has gotten fairly hard to read! Before we take it any further, let's refactor it to make it more clear what it's doing.
Here, we will use the $define
and $defcond
keywords
to define new aliases for some of our longer expansions and conditions.
This will make our intent more clear, and hopefully easier to read.
#![allow(unused)] fn main() { use derive_deftly::define_derive_deftly; define_derive_deftly! { Constructor: // First, we define some aliases. // (1) ${define FUNC_NAME { ${if tmeta(constructor(newfn)) { ${tmeta(constructor(newfn)) as ident} } else { new }} }} // (2) ${defcond F_DFLT fmeta(constructor(default))} // (3) ${define F_INITIALIZER ${if F_DFLT { ::std::default::Default::default() } else { $fname }} } // Now that's out of the way, here is the template expansion: impl<$tgens> $ttype where $twheres { $tvis fn $FUNC_NAME( $( ${when not(F_DFLT)} $fname: $ftype , ) ) -> Self { Self { $( $fname: $F_INITIALIZER , ) } } } } use derive_deftly::Deftly; #[derive(Deftly)] #[derive_deftly(Constructor)] struct Foo { #[deftly(constructor(default))] s: Vec<String>, n: u32, } }
In the example above, we've introduced some aliases for our trickier expressions, so that our template is easier to read.
At // (1)
and // (3)
, we've used $define
to introduce new expansions.
The earlier one ($FUNC_NAME
) is the name of our function;
the later one ($F_INITIALIZER
) is the expression that we use
to initialize the current field.
At // (2)
, we've used $defcond
to introduce a new condition, called $F_DFLT
.
It is true whenever we have been told
to use the Default
value for a the current field.
The $defcond
and $define
keywords
don't expand to anything themselves;
their only affect is to create the interpretation
of the keywords or conditions they define.
NOTE: To avoid name conflicts, aliases created by
$define
and$defcond
must be inALL_CAPS
.As a general practice, it's a good idea to prefix field-specific aliases with
F_
and variant-specific aliases withV_
.
How does the scoping work here?
You might have noted that, in our example above,
we define our new keywords and conditionals
at the start of our template.
In this context, there is no "current field"—
but two of our definitions use fmeta
,
which requires the existence of a current field.
How does this work?
In brief:
the body of a definition is interpreted
at the point where it is used.
No matter where we put the ${define F_INITIALZER ...}
in our template,
it will use the value of ${fmeta ...}
from the point where we actually use $F_INITIALIZER
.
Example 3: Tracking the difference between objects
So far we've seen some of derive-deftly
's features
for defining different kinds of templates.
In this section, we'll extend on what we've learned already to build a template to track the difference between objects. Unlike our previous examples, this template will define a new "difference" type that mirrors the structure of the type we're applying it to.
Here's our motivating example.
Let's suppose we have some complex structures
and we want to expose the differences between those structures
in a human-readable way.
For example, we might want to write an assert_eq!
test
that shows only the actual changed fields between two instances,
rather than showing their entire Debug outputs.
This example is inspired by the
comparable
crate, which has a bunch of other useful features we won't be replicating here. If this code seems like something you'd want, you should probably usecomparable
instead.
To begin with,
let's assume our crate has defined a Diff
trait,
looking something like:
#![allow(unused)] fn main() { pub trait Diff { /// A type that describes the difference between /// two values of this type. type Difference: std::fmt::Debug; fn diff(&self, other: &Self) -> Option<Self::Difference>; } macro_rules! assert_no_difference { { $a:expr, $b:expr } => { match $a.diff($b) { Some(diff) => panic!("{} != {}: {:?}", stringify!($a), stringify!($b), diff), None => {}, } } } }
We'll also assume that we've implemented our Diff
trait
on a wide array of types from std
.
What kind of behavior do we want here?
When we write a template to derive Diff
on a struct MyStruct
,
we'll want our template to define a new struct whose fields represent
the changes between the old structure and the new.
#![allow(unused)] fn main() { pub trait Diff { type Difference: std::fmt::Debug; } impl Diff for String { type Difference = (); } struct SomeOtherStruct; impl Diff for SomeOtherStruct { type Difference = (); } // Example struct struct MyStruct { a: String, pub b: SomeOtherStruct } // Expected output #[derive(Debug)] struct MyStructDiff { a: Option< <String as Diff>::Difference >, // Let's assume we want the same visibility. pub b: Option< <SomeOtherStruct as Diff>::Difference >, } }
And when we write a template to derive Diff
on an enum MyEnum
,
we'll want our template to define a new enumeration representing the change.
#![allow(unused)] fn main() { pub trait Diff { type Difference: std::fmt::Debug; } impl Diff for String { type Difference = (); } impl Diff for u32 { type Difference = (); } #[derive(Debug,Clone)] struct SomeOtherStruct; impl Diff for SomeOtherStruct { type Difference = (); } // Example enum #[derive(Clone, Debug)] enum MyEnum { A, B(String, String), C { v1: u32, v2: SomeOtherStruct, } } // Expected output #[derive(Debug)] enum MyEnumDiff { // We'll never actually generate this variant: // if two values are both `MyEnum::A`, they have no difference. BothA, /// Both items are MyEnum::B, but some value changed. BothB( Option< <String as Diff>::Difference >, Option< <String as Diff>::Difference >, ), // Both items are MyEnum::C, but some value changed. BothC { v1: Option< <u32 as Diff>::Difference >, v2: Option< <SomeOtherStruct as Diff>::Difference >, }, // The two items are different variants of MyEnum. // // (This assumes that MyEnum implements Debug and Clone.) TypeChanged { original_value: MyEnum, new_value: MyEnum, } } }
Let's try to see how we can make this happen!
Some difficulties when creating our Diff type
In the last section, we saw what we want
our derive_deftly(Diff)
template to do.
Here we'll analyze a few problems that we need to solve
in order to make it work.
These problems mostly derive from the fact
that we're trying to write a single template
that can either generate a struct
or an enum
,
depending on the type we're applying it to,
but they all take different forms.
Problem 1: What keyword do we use to define?
We want an expansion that gives us struct
for a struct
and enum
for an enum.
While we could trivially build one with
${if is_struct {struct} else {enum}}
,
it seems kind of verbose.
Problem 2: How do we declare the generics?
When we define our new FooDiff
struct,
we want it to have the same generics that were used
when we declared Foo
.
We might think of doing this with something like:
struct $<$tname Diff><$tgens> where $twheres { ... }
And that might be good enough for many purposes,
but it isn't quite right.
Here's why:
If Foo
is defined as struct Foo<L:Display=Bar>
,
then FooDiff
will only get defined as struct Foo<L:Display>
.
Thus the corresponding Diff
type for Foo
will not be FooDiff
, but rather FooDiff<Bar>
.
We need a way to declare generics along with their defaults.
Problem 3: Extra braces on the enum case.
For the enum case, we need our template to generate:
enum FooDiff { Variant { ... }, Variant { ... }, ... }
But for the struct case, we want:
struct FooDiff { ... }
Note that there are extra braces in the enum
case.
How can we generate those?
(We can't use ${if}
to insert a single brace,
since all the arguments to a derive-deftly
expansion
need to have balanced braces.)
Problem 4: Defining our structs and variants
Earlier
we saw
that when constructure and destructing,
we could pretend that tuple structs and tuple variants
were really braced
variants with members named 0
, 1
, 2
, and so on;
and that we could pretend that unit structs and unit variants
were just empty braced
structs.
But this approach doesn't work when declaring a struct or variant:
struct Foo {}
is not the same as struct Foo;
,
and struct Foo { 0: u32 }
is a syntax error!
So we will need a different approach. We'll need to generate braces and field names if the struct/variant has named fields; we'll need to generate parentheses and no field names if the struct/variant is a tuple; and we'll need to generate nothing at all if we have a unit struct/variant.
(Moreover, if we're generating a tuple struct or a unit struct, we'll need a trailing semicolon, but if we're generating any kind of a variant, we'll need a trailing comma.)
Problem 5: field visibility works differently among structs and enums
We decided above that we wanted each "difference" field to have the same visibility as the original field in our struct and enum.
Earlier,
we saw $fvis
as a way to get the visibility of a field.
But this won't work when we're declaring an enum
:
since every enum field is public,
$fvis
for an enum's fields always expands to pub
,
and Rust won't allow us to write
enum FooDiff {
Variant { pub a: u32; },
}
A brute-force approach for applying to structs and enums
One way we can solve our problems
with writing a derive_deftly(Diff)
template
is a brute-force approach.
Here we just use conditionals
to write different versions of our template
for the struct
case and the enum
case.
To handle the difference between tuple and unit structs and variants,
we can use conditionals as well.
Here are the new conditions we'll need:
is_enum
- True if the top-level type is an enum.is_struct
- True if the top-level type is a struct.v_is_unit
- True if the current variant (or the top-level type, in the case of a struct) is a unit struct/variant.v_is_tuple
- True if the current variant (or the top-level type, in the case of a struct) is a tuple struct/variant.v_is_named
- True if the current variant (or the top-level type, in the case of a struct) is a struct/variant with named fields.
With these conditions, we can write our template, albeit with quite a lot of redundancy.
Note 1: You may want to refresh your memory about
${select1}
and$<pasting>
.Note 2: We're ignoring generics in this example, for clarity.
#![allow(unused)] fn main() { pub trait Diff { type Difference: std::fmt::Debug; } impl Diff for u32 { type Difference = (); } use derive_deftly::{define_derive_deftly, Deftly}; define_derive_deftly!{ Diff: ${define DIFF_TYPE { Option< <$ftype as Diff>:: Difference> } } ${select1 is_struct { ${select1 v_is_unit { #[derive(Debug)] $tvis struct $<$tname Diff>; } else if v_is_tuple { #[derive(Debug)] $tvis struct $<$tname Diff>( ${for fields {$DIFF_TYPE , }} ); } else if v_is_named { #[derive(Debug)] $tvis struct $<$tname Diff> { $( $fvis $fname: $DIFF_TYPE, ) } }} // end select1 } else if is_enum { #[derive(Debug)] $tvis enum $<$tname Diff> { $( ${select1 v_is_unit { // nothing to do; we don't emit anything here. } else if v_is_tuple { $<Both $vname>( ${for fields {$DIFF_TYPE , }} ), } else if v_is_named { $<Both $vname> { $( $fname: $DIFF_TYPE, ) }, }} // end select1 ) // end iteration over variants TypeChanged { original_value: $ttype, new_value: $ttype, }, } }} // end select1 impl Diff for $tname { type Difference = $<$tname Diff>; // ... and at this point, you still have to define // an implementation for `fn diff()`: good luck! } } 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 } } }
This is a viable approach, but not a very maintanable one:
look how we've had to copy out our definition separately
for every possible Rust syntax!
We managed to save some repetition with $define
,
but we can still do better.
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.
Other features
derive-deftly
has many more features,
that aren't yet explained in this tutorial.
For example:
-
fvis
,tvis
, andapprox_equal
, more conditions for dealing with various cases by hand. -
$tdeftype
for defining a new data structure in terms of features of the input data structure, and$Xattrs
for passing through attributes.
Full details are in the reference, which also has a brief example demonstrating each construct.