Newtype and traits of the inner type

Hi there. I love strong and descriptive types and so I find myself using the newtype pattern a lot:

pub struct MyType(InnerType)

One pain point is that MyType doesn't implement the traits that InnerType does, and so I find myself reimplementing a whole lot of traits that simply delegate to the inner type:

impl AParticularTrait<Self> for MyType {
    fn the_fn(&self, other: &Self) -> bool {
        self.0.the_fn(&other.0)
    }
}

This is painful. Is there a crate that does some magic around this? Is there a better way?

I can't use

pub type MyType = InnerType

because isn't sufficient for hanging a serde (de)serialisation strategy off unfortunately.

Thanks!

Maybe the procedural macros from the ambassedor crate could work for you? (I never used this crate before but I remember it being mentioned favourably in a different discussion.)

3 Likes

Thanks @jofas. That looks promising, but I can't quite get the magic incantations so I've asked there: trying to delegate `std::ops::Add` · Issue #48 · hobofan/ambassador · GitHub

1 Like

According to this SO it's considered bad practice, but you could possibly get some mileage from implementing Deref. The warnings against sound pretty worth heeding though. rust - Is it considered a bad practice to implement Deref for newtypes? - Stack Overflow

1 Like

Thanks @jwiling

FWIW, I've decided that an impl of the trait that delegates to the inner type is a lot of syntax, but the is probably the simplest way forward.

I use the MyType { inner } a bunch… as does everyone it seems. I take the position that Deref is precisely useful for this reason. It conveys intent, is useful for accessing underlying functionality etc… T -> U should be avoided, but more often I’m using new type for T+ -> T.

So, I don’t fully disagree with the stackoverflow post, but believe the person landed in the wrong place. Take a look at the author’s last sentence; the author uses Deref for precisely this reason despite the 5 paragraph description for why not. Public versus private use?...I don’t buy it as strongly as it was stated.

1 Like

Delegating traits like Add is awkward because it's often not clear what the semantics should be. If you have a struct MyInteger(u32), then exactly which Add implementation(s) do you want?

  • Do you want to be able to add two MyIntegers together? If so, should the return type be u32, or MyInteger?
  • Do you want to be able to add a MyInteger and a u32? If so, again, should the return type be u32 or MyInteger?
  • Are there any special requirements for MyInteger that mean that an arbitrary u32 result can't necessarily be a MyInteger?

If you just delegate all trait implementations directly (either manually, with some macro crate, or by implementing Deref) then you're stuck with one possible set of behaviours: everything is going to return the types that the underlying type returns, etc. The same kinds of problem can show up with anything that involves Self, not just binary operators.

This is why implementing Deref on newtypes is not always a good idea, because it's all-or-nothing; you're asserting that it makes sense to use this type anywhere you would use the wrapped type, and if that were actually true in all cases, then it could just be a type alias.

If your newtype wrapper exists purely to have a custom serializer, and it would otherwise be fine to just use a type alias, then delegating everything or using Deref is probably fine - you only need to ensure that it's the "right" newtype in the specific context where it's going to be serialized, and elsewhere in the code where there's no serialization happening it doesn't matter if it "degrades" to the wrapped type.

When newtypes are being used for type safety, though, this usually means that the wrapper is applying some restriction that does not allow all possible values of the wrapped type. NonZeroU8 does not deref to a u8 - it could, it would not actually break anything or be unsafe. But.. it would be weird to use; doing arithmetic on NonZeroU8 and u8 in any combination would work, but would always result in a u8, and any code that wanted to continue asserting that it was nonzero would have to keep re-constructing the wrapper. When newtypes are being used this way then usually it's more ergonomic for things to "stay" in the domain of the newtype by default, and for conversion to the wrapped value to be an explicit operation that is only applied when it's needed.

2 Likes

@tornewuff solid points. I’m not sure “it’s all or nothing” though. Using it as I do I can specify which functions require the augmentation. I also have to instantiate with intent and understanding that a distinction exists. The compiler helps remind/enforce it. I’m not going to strongly disagree, but calling the use as an anti-pattern seems off.

1 Like

(fantastic thread - thanks both @tornewuff and @EdmundsEcho!)

1 Like