Expand description
§Nype - Newtype library for Rust
Nype is a library of declarative macros to help you define newtype wrappers. This library is transparent: it does leak into your public API and could be replaced at any time with manual implementations.
Nype uses declarative macro (as opposed to procedural macros) to reduce compile times. The trade-off is a somewhat less flexible syntax: the main result is that options must be defined in alphabetical order.
By default, Nype has no dependencies and support no-std
environment. A
wide range of traits can be added through the different features.
§Newtype pattern and benefits
The goal of the newtype pattern is to wrap an existing type into a new type. This enables a few benefits:
- The newtype wrapper is fully owned by the crate where it is defined. It allows the corresponding crate to implement any trait it wants without being restricted by the orphan rule.
- The newtype wrapper may carry extra semantics. Construction of the wrapper can be checked to enforce invariants.
- The newtype gets its own identity so this enables stronger compiler checks and better documentation for users.
As an example, you may define the newtype Username
wrapping a String
:
pub struct Username(String);
In this example, the inner value is private, this means that you must provide a constructor, which can also enforce extra checks:
impl Username {
pub fn new(value: String) -> Result<Self, String> {
// Enforce that a username is restricted to ASCII alphanum chars
let is_valid = value
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_');
if is_valid {
Ok(Self(value))
} else {
Err(String::from("invalid input, alls characters must be ASCII alphanum"))
}
}
}
You can also define functions that take Username
as an argument instead
of String
. This stronger typing helps to avoid errors: the compiler will
make sure that you don’t mix-up values and pass a valid username. Example:
fn generate_greeting_email(username: &Username) -> EmailTemplate {
todo!();
}
fn main() {
let email_address = String::from("john.doe@example.com");
let username = Username::new(String::from("john_doe")).expect("constant username is valid");
let _ = generate_greeting_email(&username); // ok
// let _ = generate_greeting_email(&email_address); // rejected by the compiler
}
You’re able to define your own traits and methods directly on the newtype,
as opposed to being limited to the methods defined on String
.
impl Username {
pub fn get_mock_value(some_rng: ...) -> Self {
match some_rng.rand(0..5) {
0 => Self::new(String::from("Alice")).expect("mock value is valid"),
1 => Self::new(String::from("Bob")).expect("mock value is valid"),
2 => Self::new(String::from("Charlie")).expect("mock value is valid"),
3 => Self::new(String::from("Dan")).expect("mock value is valid"),
_ => Self::new(String::from("Eve")).expect("mock value is valid"),
}
}
}
The newtype pattern can be applied to any value. It is most commonly used to define domain types. In particular it can enforce checks on strings, integers, enums, ids, etc.
§Drawbacks of the newtype pattern
There are two main drawbacks to the newtype pattern.
- Compatibility
- Boilerplate
The fact that newtypes get their own identity is a benefit as it enables
stronger type checks, but also a drawback as now your type is distinct.
If we follow with the Username
example from the previous section, it can
no longer be passed directly to functions from the standard library or
ecosystem that expect a String
: you need a conversion step first (either
explicit or through some extra traits/methods). In practice the benefit of
stronger identity are usually worth it, as long as conversion to more common
types are implemented.
The second drawback is boilerplate. Implementing a reliable and easy-to-use
newtype can invovle a fair amount of boilerplate. For example, you should
implement conversions as discussed just before. But, you also need custom
deserializers to enforce that the checks are performed during deserialization
too. You should also have dedicated errors for failures. There could also
be some performance concerns: for example the Username
example requires
ownership and allocation of a string, which could cause extra cloning.
This extra boilerplate is a major reason why the newtype pattern may not be used enough. This is where Nype steps in: it provides a set of macros to define high quality newtype wrappers while keeping the boilerplate to a minimum.
§Nype macros
Nype defines the following macros:
- [
define_new_string
]: Define a string-like newtype wrapper.
Modules§
Macros§
- define_
string_ type - Define a newtype wrapper for a string.