Impl-tools: beyond derive
Allow me introduce the impl-tools crate,
by discussing the limitations of #[derive]
.
Deriving Default
...
... over a method
Frequently, a type's public API includes a fn new() -> Self
constructor, with
Default
implemented over this. impl_default
lets us skip some boilerplate:
#![allow(unused)] fn main() { use impl_tools::impl_default; #[impl_default(Foo::new())] pub struct Foo { // fields here } impl Foo { pub fn new() -> Foo { Foo { /* fields here */ } } } let foo = Foo::default(); }
... for an enum
Similarly, we can derive Default
for enums:
#![allow(unused)] fn main() { #[impl_tools::impl_default(Option::None)] pub enum Option<T> { None, Some(T), } // (Yes, the impl is correct with regards to generics — see below.) let x: Option<std::time::Instant> = Default::default(); }
... with specified field values
Lets say we want to implement Default
for a struct with non-default values for
fields:
#![allow(unused)] fn main() { struct CarStats { num_doors: u8, fuel_is_diesel: bool, fuel_capacity_liters: f32, } }
Wouldn't it be nice to be able to specify our default values in-place? We can, if we re-write using impl-tools:
#![allow(unused)] fn main() { use impl_tools::{impl_scope, impl_default}; impl_scope! { #[impl_default] struct CarStats { num_doors: u8 = 3, // specified default value fuel_is_diesel: bool, // no initializer: uses type's default value fuel_capacity_liters: f32 = 50.0, } } }
Note that field: Ty = val
is not (currently) Rust syntax. The
impl_scope
macro has special support for this, besides other functionality.
Deriving Debug
: ignoring hidden fields
For example, let us consider Lcg64Xsh32
(also known as PCG32). This is a simple random number generator, and as per policy of the RngCore
trait, does not print out internal state in its Debug
implementation.
#![allow(unused)] fn main() { #[derive(Clone, PartialEq, Eq)] pub struct Lcg64Xsh32 { state: u64, increment: u64, } // We still implement `Debug` since generic code often requires it use std::fmt; impl fmt::Debug for Lcg64Xsh32 { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "Lcg64Xsh32 {{}}") } } }
Using impl-tools
we could reduce this to:
#![allow(unused)] fn main() { #[impl_tools::autoimpl(Debug ignore self.state, self.increment)] #[derive(Clone, PartialEq, Eq)] pub struct Lcg64Xsh32 { state: u64, increment: u64, } }
(This applies equally to fields which do not themselves implement Debug
.)
Deriving Deref
How could we derive Deref
and DerefMut
? By using a specified field:
#![allow(unused)] fn main() { #[impl_tools::autoimpl(Deref, DerefMut using self.animal)] struct Named<A> { name: String, animal: A, } }
Generics
An experienced Rustacean should know that #[derive]
makes some incorrect
assumptions with regards to generics. For example,
/// Implements Clone where T: Clone (correct)
#[derive(Clone)]
enum Option<T> {
None,
Some(T),
}
/// Implements Clone where T: Clone (unnecessary bound)
#[derive(Clone)]
struct Shared<T> {
inner: std::rc::Rc<T>,
}
/// Attempts to implement Clone where T: Clone
/// (error: Clone for Cell<T> requires T: Copy)
#[derive(Clone)]
struct InnerMutable<T> {
inner: std::cell::Cell<T>,
}
The #[autoimpl]
macro takes a different approach: do not assume any bounds,
but allow explicit listing of bounds as required.
#![allow(unused)] fn main() { use impl_tools::autoimpl; // Note: autoimpl does not currently support enums (issue #6) // No bound on T assumed #[autoimpl(Clone)] struct Shared<T> { inner: std::rc::Rc<T>, } // Explicit bound on T #[autoimpl(Clone where T: Copy)] struct InnerMutable<T> { inner: std::cell::Cell<T>, } }
To simplify the most common usage and to cover the case where multiple traits
are implemented simultaneously, the keyword trait
may be used as a bound:
#![allow(unused)] fn main() { use impl_tools::autoimpl; #[autoimpl(Clone, Debug, Default where T: trait)] struct Wrapper<T>(T); }
Auto trait implementations
Lets say you write a trait, and wish to implement that trait for reference types:
#![allow(unused)] fn main() { trait Greet { fn greet(&self, name: &str); } impl<T: Greet + ?Sized> Greet for &T { fn greet(&self, name: &str) { (*self).greet(name); } } // Also impl for &mut T, Box<T>, ... }
This can be quite tedious, enough so that macros (by example) are often used to deduplicate the implementations. But why should we have to write even the first implementation? It's all trivial code!
#![allow(unused)] fn main() { use impl_tools::autoimpl; // One line to do it all: #[autoimpl(for<T: trait + ?Sized> &T, &mut T, Box<T>)] trait Greet { fn greet(&self, name: &str); } // A test, just to prove it works: impl Greet for String { fn greet(&self, name: &str) { println!("Hi {name}, my name is {self}!"); } } let s = "Zoe".to_string(); s.greet("Alex"); (&s).greet("Bob"); Box::new("Bob".to_string()).greet("Zoe"); }
Limitations
To conclude this article, let us briefly discuss limitations.
Support other standard traits: should be easy. #[autoimpl]
currently supports
only the most-frequently-used subset of traits supported by #[derive]
(plus
Deref
and DerefMut
). The crate should likely support everything that
#[derive]
does by default, and possibly more (see
derive_more
).
Extensibility: #[autoimpl]
could be useful for user-defined traits;
impl_scope
could be useful for user-defined attribute macros. It would be
nice if these could be extended in the same way as #[derive]
is, but that is
impossible (without direct support from Rust itself). Instead, the impl-tools
crate is split in two: impl-tools-lib
(contains all functionality) and
impl-tools
itself (just a thin proc-macro
driver). An alternative driver
containing additional functionality may be used instead of impl-tools
.
Exemplar: kas-macros
(from v0.11).