1 unstable release

0.0.0 Nov 6, 2023

#74 in #newtype

MIT/Apache

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:

  1. Lifetimes an generic can be computed from the newtype struct and the trait.
  2. The for NewType part is redundant as we know we want to implement it for the given type.
  3. The constraints can be computed from the trait and newtype definition as well
  4. 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 example AsRef will not reference to the Inner type but to all types the inner type implements AsRef 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()}
  • fn name(&mut self) -> Type
    • For unknown functions this translates to fn name(&mut self) -> Type {self.0.name()}
  • fn name(&xx) vs fn 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 with Other::new(inner). This allows to complete functions like fn new_arc -> Arc<Self> which will expand to fn 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 default From 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:

    1. inner_mut must be unsafe because it can be used to break the invariants. This is automatically added.
    2. The inner must not be pub when invariants are used. This will result in a compile error.
    3. Default traits that return mutable references are disabled.
    4. The programmer is responsible for enforcing the invariants in any custom function and trait implementation.
  • fn invariant_tryfix(&mut Inner) -> bool {/*required implementation */}
    Like invariant_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

No runtime deps