Newtype
There's a great Zero Cost abstraction that we'll be using a lot that you might not already be familiar with: we're talking about the "Newtype Pattern"!
Now, I told you to read the Rust Book before you read this book, and I'm sure you're all good students who wouldn't sneak into this book without doing the required reading, so I'm sure you all remember exactly what I'm talking about, because they touch on the newtype concept in the book twice, in two very long named sections:
- Using the Newtype Pattern to Implement External Traits on External Types
- Using the Newtype Pattern for Type Safety and Abstraction
...Yeah... The Rust Book doesn't know how to make a short sub-section name to save its life. Shame.
Newtype Basics
So, we have all these pieces of data, and we want to keep them separated, and we don't wanna pay the cost for it at runtime. Well, we're in luck, we can pay the cost at compile time.
# #![allow(unused_variables)] #fn main() { pub struct PixelColor(u16); #}
Ah, except that, as I'm sure you remember from The
Rustonomicon
(and from the
RFC
too, of course), if we have a single field struct that's sometimes different
from having just the bare value, so we should be using #[repr(transparent)]
with our newtypes.
# #![allow(unused_variables)] #fn main() { #[repr(transparent)] pub struct PixelColor(u16); #}
Ah, and of course we'll need to make it so you can unwrap the value:
# #![allow(unused_variables)] #fn main() { #[repr(transparent)] pub struct PixelColor(u16); impl From<PixelColor> for u16 { fn from(color: PixelColor) -> u16 { color.0 } } #}
And then we'll need to do that same thing for every other newtype we want.
Except there's only two tiny parts that actually differ between newtype declarations: the new name and the base type. All the rest is just the same rote code over and over. Generating piles and piles of boilerplate code? Sounds like a job for a macro to me!
Making It A Macro
The most basic version of the macro we want goes like this:
# #![allow(unused_variables)] #fn main() { #[macro_export] macro_rules! newtype { ($new_name:ident, $old_name:ident) => { #[repr(transparent)] pub struct $new_name($old_name); }; } #}
Except we also want to be able to add attributes (which includes doc comments), so we upgrade our macro a bit:
# #![allow(unused_variables)] #fn main() { #[macro_export] macro_rules! newtype { ($(#[$attr:meta])* $new_name:ident, $old_name:ident) => { $(#[$attr])* #[repr(transparent)] pub struct $new_name($old_name); }; } #}
And we want to automatically add the ability to turn the wrapper type back into the wrapped type.
# #![allow(unused_variables)] #fn main() { #[macro_export] macro_rules! newtype { ($(#[$attr:meta])* $new_name:ident, $old_name:ident) => { $(#[$attr])* #[repr(transparent)] pub struct $new_name($old_name); impl From<$new_name> for $old_name { fn from(x: $new_name) -> $old_name { x.0 } } }; } #}
That seems like enough for all of our examples, so we'll stop there. We could add more things:
- Making the
From
impl being optional. We'd have to make the newtype invocation be more complicated somehow, the user puts ", no-unwrap" after the inner type declaration or something, or something like that. - Allowing for more precise visibility controls on the wrapping type and on the
inner field. This would add a lot of line noise, so we'll just always have our
newtypes be
pub
. - Allowing for generic newtypes, which might sound silly but that we'll actually
see an example of soon enough. To do this you might think that we can change
the
:ident
declarations to:ty
, but since we're declaring a fresh type not using an existing type we have to accept it as an:ident
. The way you get around this is with a proc-macro, which is a lot more powerful but which also requires that you write the proc-macro in an entirely other crate that gets compiled first. We don't need that much power, so for our examples we'll go with the macro_rules version and just do it by hand in the few cases where we need a generic newtype. - Allowing for
Deref
andDerefMut
, which usually defeats the point of doing the newtype, but maybe sometimes it's the right thing, so if you were going for the full industrial strength version with a proc-macro and all you might want to make that part of your optional add-ons as well the same way you might want optionalFrom
. You'd probably wantFrom
to be "on by default" andDeref
/DerefMut
to be "off by default", but whatever.
As a reminder: remember that macro_rules
macros have to appear before
they're invoked in your source, so the newtype
macro will always have to be at
the very top of your file, or if you put it in a module within your project
you'll need to declare the module before anything that uses it.