Newtype

Newtype is a pattern in Rust copied from other strongly typed languages. It is a wrapper around a single data type field.

#![allow(unused)]
fn main() {
struct UserId {
  user_id: i64
}
}

But commonly, it is written like:

#![allow(unused)]
fn main() {
struct UserId(i64);
}

You can access the data field like:

#![allow(unused)]
fn main() {
struct UserId(i64);
let uid = UserId(1001);
let _ = uid.0;
}

Why

The pattern can be considered too simple, but there are many reasons why the pattern is useful.

Semantics

When passing a value around, it is helpful to have semantic meaning associated with a type, especially with primitive values.

Take a variable which is suppose to represent a user id. It could be just an i64 type or it could be a UserId type which wraps an i64. By giving the variable a stronger type, the type can be used to enforce the proper arguments are passed to a function.

#![allow(unused)]
fn main() {
struct UserId(i64);
struct User;
fn get_user_1(user_id: i64) -> User {
  todo!()
}

fn get_user_2(user_id: UserId) -> User {
  todo!()
}
}

Any i64 value could mistakenly be passed to get_user_1, but only UserId values can be used with get_user_2. Perhaps it is not too important for a single parameter function, but if there are many parameters of the same type, it can be more beneficial.

#![allow(unused)]
fn main() {
struct Color;
fn new_color(r: f32, g: f32, b: f32, a: f32) -> Color {
  todo!()
}
}

The new_color function takes 4 f32 values but unless you are familiar with the order, the arguments could be mixed up. Or there could be an assumption that instead of RGBA, a CMYK color model is used.

Of course, having a unique type for every parameter may be overkill.

While there could be more code which the compiler has to evaluate, the Rust compiler will generally remove the wrapper type used in the newtype pattern. In other words, the UserId type becomes a zero-cost abstraction which should not impose any performance or efficiency penalty compared to the original i64 type. So the code can use a newtype to improve program correctness by strong type checking for “free” effectively.

Alternatives for Argument Passing

An alternative to the multiple same type parameters is to create a new type for the arguments like:

#![allow(unused)]
fn main() {
struct Color;
struct ColorArgs {
  r: f32,
  g: f32,
  b: f32,
  a: f32,
}

fn new_color(args: ColorArgs) -> Color {
  todo!()
}

let _ = new_color(ColorArgs { r: 1.0, g: 1.0, b: 1.0, a: 1.0 });
}

In a way, the workaround gives names to the arguments at the call site. There are various proposals for formal named argument parameters in Rust RFCs.

Trait Coherence / Orphan Rule

Implementing traits on types follows several rules. The most prominent is the orphan rule. It specifies when a trait can be implemented for a type. The basic idea is a type can only implement a trait if either the type or the trait was created in the current crate. It prevents conflicting implementations of traits on types. The actual rule is more subtle and is worth understanding (e.g. From implementations involving a local crate type into a standard library type work, and yet From and the standard library type belong to the standard library).

Using the newtype pattern, the orphan rule is “worked around”.

Suppose a foreign type in a different crate does not implement serde’s Serialize trait, but you want to serialize the type’s data.

#![allow(unused)]
fn main() {
// In a third party crate:
pub struct Coordinates {
  pub x: i32,
  pub y: i32,
}

// In local crate:
struct MyType1 {
  pos: Coordinates,
  radius: i32,
}

struct MyType2 {
  pos: Coordinates,
  len: i32,
}
}

You cannot add a Serialize implementation to Coordinates, but you can wrap the type and then implement the trait like:

#![allow(unused)]
fn main() {
pub struct Coordinates {
  pub x: i32,
  pub y: i32,
}
trait Serialize { }
// In local crate:

struct MyCoordinates(Coordinates);

impl Serialize for MyCoordinates {
  // ...
}

struct MyType1 {
  pos: MyCoordinates,
  radius: i32,
}

struct MyType2 {
  pos: MyCoordinates,
  len: i32,
}
}

The newtype pattern is one way to “work around” the orphan rule.

Multiple Implementations for Single Trait

A trait can only be implemented once for a type. So if you have a Display implementation for a Date type, there can be no other Display implementation.

#![allow(unused)]
fn main() {
use std::fmt;
use std::time::Instant;

struct Date(Instant);

impl fmt::Display for Date {
  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    todo!("Print a locale specific date")
  }
}
}

Multiple attempts at implementing a trait for a type will result in a compiler error. Sometimes the crate which declared a trait will have blanket implementations. In most cases, it is helpful to have blanket implementations, but it can lead to conflicts.

If you want to have different implementations, then multiple newtypes can be created with the original type. Each newtype can have its own trait implementation. For instance, if there is a Date type with a Display trait impelmentation, then perhaps in some usages, the local specific format should be used, and in other cases, the ISO 8601 format should be used.