Nutype: the newtype with guarantees!
Serhii Potapov February 13, 2023 #rust #macro #newtype #nutypeThe newtype pattern
I am a big fan of the newtype pattern in Rust. In my projects I use it as much as I can: it makes my code self-documented and may even help to enforce domain logic using Rust's type system.
It's not rare when I found myself writing code similar to this:
use ;
;
It is easy to grasp what it is going on here:
- We define a newtype
Email
that wrapsString
type with many traits derived. - We defined
EmailError
enum to represent the possible validation failures. - We define a public associated function
::new()
to obtainEmail
:- It performs sanitization on the input value (
.trim().to_lowercase()
) - It runs the fallible validation and returns
Ok(Email)
in case of success
- It performs sanitization on the input value (
Generally, this approach works well, however, there are a few pain points.
Pain point 1: no constraints
As long as everyone uses ::new()
to obtain an instance of Email
we can be sure that every instance of Email
in the system has an inner string value that complies with the validation rules.
But it's rather a convention, not a constraint.
Chances are high, that at some point someone will create an Email
bypassing the validation rules:
let email = Email;
or
let email = from;
Pain point 2: cumbersomeness
As I mentioned above, for the sake of type safety and documentation, I use the newtype pattern a lot. For example, a typical user structure may look like the following one:
You can imagine that defining the newtype structs, error types and validation for every single type, becomes very quickly cumbersome.
This made me seek a way to DRY my code.
Nutype: a new hope
Splitting down the problem, for most of my newtypes I just want the following:
- Define the sanitization rules
- Define the validation rules
- Derive the definition of the error automatically based on the validation rules.
So I came up with nutype library, that relies on heavy use of proc macros and does exactly what I need.
For example, the same Email
type can be defined in a much shorter and more declarative way:
use nutype;
;
The code above also defines EmailError
implicitly.
I know, this could be too much magic for someone, but I wanted to experiment with it.
What about constraints and guarantees?
One of the key features of nutype is that it disallows obtaining a value of a particular type bypassing the validation rules. To my knowledge, there is no way to do it in safe Rust. If you find any, please let me know!
We can try:
let email = Email;
and we get an error:
error[E0423]: cannot initialize a tuple struct that contains private fields
It's due to the fact, that nutype wraps the type into an extra module, similar to the following:
use Email;
This makes the inner tuple field inaccessible to the outer world.
Looking ahead, I will say that it is impossible to obtain an invalid Email
even by deriving DerefMut
and modifying an instance of a valid email.
You can try it yourself.
As result, we can firmly rely on the types: whenever we have a value of type Email
, we can be confident that it's indeed a valid email!
What is next?
As for now, nutype
can work with serde (requires serde1
flag). But I'd like it to play well with some other major crates in the ecosystem too.
Arbitrary support
In the future, I'd like to add support for Arbitrary crate.
Consider the following example:
;
This would automatically derive Arbitrary
trait to generate only valid values for Age within the range 18..=99
.
Although, I guess it will be impossible to marry Arbitrary
with custom validation rules.
Deriving Ord and Eq for floats
If you've worked with float types (f32
, f64
) you have learned by now, they have no Ord
nor Eq
traits implemented.
This causes some pain: it's not possible to sort Vec<f64>
or use BTreeSet<f64>
. Both require Ord
trait.
The reason why the float types do not implement Ord
and Eq
is because of special float variants like f64::INFINITY
or f64::NAN
. There is a function
is_finite
that does the check against those variants.
It must be possible to filter out Infinity
and NaN
with validation and enable derives of Ord
and Eq
:
;
This will come in the future versions of nutype! =)
Resources and links
- Nutype on github
- The newtype pattern in the Rust Book
- The Newtype Pattern in Rust - a very good article about the practical usage of newtype in Rust by Justin Wernick
- "Domain Modeling Made Functional" by by Scott Wlaschin - one of my favorite books about software development. I definitely got some inspiration from it.
- Elm Radio podcast: Intro to Opaque Types - another source of my inspiration.