1 unstable release
0.0.0 | Nov 6, 2023 |
---|
#76 in #newtype
4KB
63 lines
Table of Contents
WIP: This crate is work in progress, the documentation includes some PLANNED features.
The Newtype Idiom
The newtype idiom is a powerful pattern in Rust to use the type system to distinguish between different uses of the same underlying type.
Unfortunately implementing a newtype requires a lot of boilerplate code. This crate provides two macros to conveniently implement this boilerplate code for you.
You have full control over what code is generated and can add custom implementations as needed. The macros are designed to be used in a way that it works out of the box for most cases and every aspect can be refined when required.
Example
The simplest case lets you use it like this:
use newtypedecl::newtype;
// This will generate a basic API for `Example` and implement some common traits.
#[newtype]
pub struct Example(String);
// a 'new' fn is generated
let example = Example::new("foo");
// AsRef is automatically implemented for all types the inner type implements AsRef for
let reference: &[u8] = example.as_ref();
assert_eq!(reference, b"foo");
When using the attribute macro syntax will be to cluttered because of many customs control definitions are added then or one want to bulk-define a lot more newtype structs, then a alternative function-like macro syntax where the control block comes after the struct is available:
use std::ops::{Deref, DerefMut};
use newtypedecl::newtype;
#[newtype(!fn new, impl Deref, impl DerefMut)]
pub struct Example(String);
#[newtype]
pub struct Another(i32);
// and so on...
is equivalent to:
use std::ops::{Deref, DerefMut};
use newtypedecl::newtypes;
newtypes! {
pub struct Example(String) {
!fn new;
impl Deref;
impl DerefMut;
}
pub struct Another(i32);
// and so on...
}
The Newtype Structure
For syntactic unambiguity newtypes are always implemented as tuple-struct with the wrapped data member at first position. Any number zero-sized-type members acting as markers can follow as long they are trivially constructable. The wrapped data/Type will further be referred as inner/Inner in this documentation.
As any struct declaration, the macro supports attributes, lifetimes and generics.
Enforce the Newtype Idiom
By default #[repr(transparent)]
is automatically added to the generated struct. This ensures
that the struct has only one non zero sized data member. Any number of marker members are
still allowed.
When any other repr
attribute is specified then the macro will not add
#[repr(transparent)]
. Use [repr(Rust)]
to suppress #[repr(transparent)]
.
The Control Block
While the newtype macros can do a lot on their own they need some way to control how code is generated. This is done by a block of special syntax containing control directives and abbreviated rust forms.
- In the
#[newtype]
attribute macro this is passed as comma delimited lists on parenthesis eg.#[newtype(!fn new, impl Deref)]
. - In the 'newtypes!{}' function like macro the control directives are passed in a block within braces after the struct definition, entities that not end in a braced block need to be delimited by semicolons there.
The control block is optional. You can always add implementation blocks for your newtype struct and implement trait in normal rust code. For anything non-boilerplate implementing things explicitly is actually the recommended way.
Furthermore the generated code takes lifetimes, generics and constraints literally from newtype struct definition. If these need to be refined then one has to implement things explicitly.
Control Directives
We control which methods and member functions will be generated and which traits will be implemented for the newtype.
Control directives can either disable the ones that would be generated by default or define/enable those that are not enabled by default.
For disabling one adds a exclamation mark in front of its specification !fn $name
will
suppress the automatic generation of a $name
member function (although one can still define a
custom one). !impl $name
will suppress the automatic implementation of trait $name
.
There are 2 special forms: !default fn
and !default impl
will suppress the generation of
all default function and trait implementations.
The traits and functions the newtype macro knows about and implements specially are described at the end.
Abbreviated Definitions
This is where the fun starts. The newtype generator knows about a lot functions and traits and can generate the necessary code on its own. Often only abbreviated definitions are needed which become automatically completed.
Traits
One can use the normal rust syntax form for a trait implementation:
impl<'lifetimes, Generics> TraitName for Newtype
where
Constraints
{
fn trait_fn() { /*....*/ }
}
But actually most of this can in many case be left out because we know the newtype and which trait we want to implement:
- Lifetimes an generic can be computed from the newtype struct and the trait.
- The
for NewType
part is redundant as we know we want to implement it for the given type. - The constraints can be computed from the trait and newtype definition as well
- The newtype macros know about common traits and how to implement the trait functions for them to give proper 'newtype' semantics.
NOTE: not all implemented yet
impl TraitName
Is enough for the standard traits the newtype macro knows about. Usually it will create a generic implementation that makes it compatible with the respective trait implementation of the wrapped inner type. For exampleAsRef
will not reference to the Inner type but to all types the inner type implementsAsRef
for.impl TraitName<type>
When one wants a less generic implementation then it is possible to specialize on specific types.impl<T> TraitName<T> where Constraint
Adding extra constraints. Note that the generics from the newtype don't need to be passed here, the newtype macro will merge them in.impl UnknownTrait { fn traitfn(){...} }
Traits that are unknown to the newtype macros need a block for the trait functions. The newtypes generics, the `for Newtype' and where clauses are not required.
Member Functions
Unlike traits the newtype macros try to generate code even for unknown functions. This is done by simply calling the inner value with the same function. When the provided abbreviation was not sufficient, then this will lead to compiler errors later.
fn name
- For known functions the implementation is generated.
- For unknown functions this translates to
fn name(&self) {self.0.name()}
fn name -> Type
- For unknown functions this translates to
fn name(&self) -> Type {self.0.name()}
- For unknown functions this translates to
fn name(&mut self) -> Type
- For unknown functions this translates to
fn name(&mut self) -> Type {self.0.name()}
- For unknown functions this translates to
fn name(&xx)
vsfn name(xx)
Known functions can have some variants depending on if the types is passed by value or reference (or anything by dyn/impl some trait that is supported).
As noted above, when there is no body for an unknown function defined then a body that
forwards the call to the Inner type is generated. This defaults to (&self)
as parameters and
having no return, when the forwarded function is expected to return something then the return
type has to be given explicitly. Such generated forwarding functions will not inherit the
pub
visibility from the newtype struct definition.
use newtypedecl::newtypes;
newtypes! {
struct Forwarding(String) {
// calls String::len()
pub fn len -> usize;
// We need to specify `&mut self` here since `&self` is the default
pub fn clear(&mut self);
}
}
let mut n = Forwarding::new("foobar");
assert_eq!(n.len(), 6);
n.clear();
assert_eq!(n.len(), 0);
Known Functions and Traits
Functions
fn new
, fn new_*
This completes all 'new' like functions as following:
- Doc comment/attribute will be generated.
- The 'pub' visibility is inherited from the newtype struct.
- The parameters default to
impl Into<Inner>
When parameters are given then they can be empty,Inner
,&Inner
,impl Into<Inner>
,dyn Into<Inner>
. - The return type defaults to
Self
. Other wrappers are supported but should be constructable withOther::new(inner)
. This allows to complete functions likefn new_arc -> Arc<Self>
which will expand tofn new_arc(inner: impl Into<Inner>) -> Arc<Self> {Arc::new(inner.into())}
. - When no body is given then it is completed using the information gathered from above.
fn new(inner: impl Into<Inner>) -> Self
is default generated. To suppress this add !fn new
to the control block or refine the abbreviation.
Traits
AsRef
and AsMut
These traits are implemented by default, to suppress them add !impl AsRef
and/or !impl AsMut
to the control block.
The default implementations implement AsRef
/AsMut
for any type that the inner type of the
newtype implements them:
impl<T> AsXXX<T> for Inner
where
Inner: AsXXX<T> {...}
Borrow
and BorrowMut
These traits are not implemented by default, to enable them add impl Borrow
and/or impl BorrowMut
to the control block.
The default implementations are similar to the AsRef
/AsMut
implementation above.
use std::borrow::{Borrow, BorrowMut};
use newtypedecl::newtype;
#[newtype(impl Borrow, impl BorrowMut)]
pub struct Example(i32);
let mut s = Example::new(1234);
*s.borrow_mut() = 3456;
let r: &i32 = s.borrow();
assert_eq!(*r, 3456);
Deref
and DerefMut
These traits are not implemented by default, to enable them add impl Deref
and/or impl DerefMut
to the control block. DerefMut
requires Deref
.
Implementing these traits need some thoughts, because the newtype idiom is meant to hide underlying types and use the type system to enforce each newtype to be treated uniquely. Dereferencing in turn makes types appear as some other type, often this is not desired when using newtypes.
The default implementations defines Deref
for any type the inner type implements it for.
impl<T> Deref for Inner
where
Inner: Deref<Target = T>,
{
type Target = <Inner as Deref>::Target;
...
}
DerefMut
depends on Deref
and has a rather simple implementation that just returning
&mut self.0
.
use newtypedecl::newtype;
use std::ops::Deref;
#[newtype(impl Deref)]
struct DerefTest(String);
let n = DerefTest::new("foobar");
assert_eq!(&*n, "foobar");
From
Like fn new
the From
trait is implemented for all types the inner type implements From
for. This trait is implemented by default, to disable it add !impl From
to the control
block. A custom implementation or disabling From
is required when a newtype is generic as in
struct NewType<T>(T)
because the default implementation will conflict with the stdlib
impl<T> From<T> for T
blanket implementation.
use newtypedecl::newtype;
#[newtype]
struct FromTest(String);
let n = FromTest::from("foobar");
assert_eq!(n.0, "foobar");
Limitations
- The
From
trait won't work with a generic inner. In future this shall be detected and disable/fix the defaultFrom
implementation. - Trait matching works on the identifier name, giving a full path fails 'impl std::ops::Deref' is not recognized, this will be fixed in future versions.
- Boxed or otherwise encapsulated inner types like
struct BoxedInner(Box<Inner>)
are not handled yet, this is a bug and will be fixed asap.
PLANNED: Functions
Not yet implemented, check back for new versions. Send PR or open a ticket for anything else you are missing.
-
fn inner
,fn inner_mut
Return references to the inner type -
fn into_inner
Consumes self, returns the inner type -
fn invariant_check(&Inner) -> bool {/*required implementation */}
Validates the value of the inner type. When present then other functions/traits will use them. Also defines some utility functions:fn assert_invariant(&Inner)
,fn invariant(Inner) -> Inner
panics on invariant failure.fn try_invariant(Inner) -> Result<Inner, newtypedecl::InvariantError>
Return an error on invariant failure
To uphold invariant guarantees some conditions must be met:
inner_mut
must be unsafe because it can be used to break the invariants. This is automatically added.- The inner must not be pub when invariants are used. This will result in a compile error.
- Default traits that return mutable references are disabled.
- The programmer is responsible for enforcing the invariants in any custom function and trait implementation.
-
fn invariant_tryfix(&mut Inner) -> bool {/*required implementation */}
Likeinvariant_check()
but can try to fix the inner to conform with the invariant. When defined this is used whenever a mutable inner is available.
PLANNED: Traits
Not yet implemented, check back for new versions. Send PR or open a ticket for anything else you are missing.
TODO: test and document compatibility with derive_more
TryFrom
FromStr
ToOwned