newtypedecl 0.0.1

declarative newtype macro
Documentation
  • Coverage
  • 100%
    14 out of 14 items documented1 out of 13 items with examples
  • Size
  • Source code size: 307.0 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 392.5 kB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 3s Average build duration of successful builds.
  • all releases: 8s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • Repository
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • cehteh

WARNING: until v0.1.0 this crate is in brainstorming phase, expect major breaking changes

The newtype pattern is a powerful idiom in Rust to use the type system to distinguish between different uses of the same type.

https://doc.rust-lang.org/rust-by-example/generics/new_types.html

Unfortunally implementing the newtype pattern requires some boilerplate code. This Crate provides the [newtype!] declarative macro and traits to conveniently implement this boilerplate code for you.

How it works

We define newtypes as tuple structs with only one single data member. Thus it's type, lifetimes, generics are all well known and can be reused when implementing member functions and traits for the newtype instead typing repetive code. Think about it as code-completion at compile time.

Additionally the user can define a invariant that must must hold at any time. This invariant will be checked whenever a newtype is constructed and at some other (unspecified) places. This invariant is a contract that must never be broken. Newtypes are not a refinement type system, careless usage and trait implementations can lead to cases where the contract can be possibly broken on misuse.

newtypedecl augments derive, where approbiate one can and should use derive. The newtype macro addresses things that are commonly not covered by derive macros.

Simple Example

The simplest case lets you use it like this:

#[macro_use]
use newtypedecl::newtype;

newtype! {
    /// Just a wrapper around a string
    pub struct Example(String);
}

Terminating a newtype with a semicolon will implement a preselected (opinionated) set of methods and traits for it. See the [newtype!] macro docs for a list what this implements by default.

Control Block Example

When using a control block in braces instead the terminating semicolon after a the newtype definiton we gain some DWIM superpowers:

#[macro_use]
use newtypedecl::*;

newtype! {
    /// The example from above with some more features
    #[derive(Debug, Hash, Clone)]
    pub struct Example(String){
        // invariant that must always be held
        invariant(value) {!value.is_empty()}
        // Define the traits and functions like above
        impl Deref + AsRef + Borrow + From;
        pub fn new;
        fn inner + into_inner + inner_mut;

        // More:
        // implementing Default takes a block of code that
        // returns something that can be converted `.into()` a inner value
        impl Default { "NULL" }
        // custom functions can be placed right here
        pub fn print_value(&self) {println!("{}", **self);}
    }
}

let example = Example::default();
assert_eq!(*example, "NULL");
example.print_value();

Principles

The macro tries the most generic applicable form of methods and trait implementations. This usually means that parameters are impl Into or impl AsRef. Future versions may introduce dyn dispatch.

Dangerous usage

Whenever a newtype is not only cosmetic but implements some contract which must be fulfilled at any time, either by just documenting this or by adding invariants to the newtype there is a danger that misuse breaks this contracts when a reference to the inner value is reachable. This not only covers mutable references. Immutable references are subject of this problem too when the inner value provides some kind of interior mutability.

The only way to address these problems is by being careful of what interfaces and traits a newtype implement. This is not something this library can enforce, we only try to accomplish this in a best-effort base and leave the actual decisions to the implementor.

When you want to be very safe then do not implement traits that expose a reference to the inner. Only provide methods that do safe access and transformations.

Panics

Constructing a newtype from a inner value can fail the invariant check. The 'try' forms try_new() and TryFrom handle this and will return a runtime error in invariant failure. Other forms that take a raw inner to be inserted into a newtype will panic in case the invariant check fails. Thus users should not implement the traits and functions that infallibly construct or alter a newtype when invariants are defined.

The invariant must holds at any time, never make the assumption it is ok to store an invalid value not even temporaly.