Rust is a powerful programming language that emphasizes safety and performance. It is known for its strong type system, which helps to prevent common programming errors. One of the features of Rust's type system is the "New Type" idiom, which can be used to create a new type that is similar to a struct with one field. In this article, we will explore the New Type idiom and compare it to a struct with one field.
First, let's start by defining a struct with one field. Here is an example:
struct Point {
x: f64,
}
In this example, we have defined a struct called Point that contains a single field called x. The type of the x field is f64, which is a 64-bit floating-point number. We can create a new instance of the Point struct like this:
let p = Point { x: 3.0 };
Now, let's compare this to the New Type idiom. The New Type idiom is a way to define a new type that is a wrapper around an existing type. Here is an example:
struct Millimeters(f64);
In this example, we have defined a new type called Millimeters that contains a single field called 0. The type of the 0 field is f64, which is the same as the type of the x field in the Point struct. However, we have defined a new type called Millimeters that is distinct from the f64 type. We can create a new instance of the Millimeters type like this:
let m = Millimeters(3.0);
At first glance, it might seem like the Millimeters type is similar to a struct with one field. However, there are some important differences between the two. The main difference is that the Millimeters type is a distinct type, whereas the Point struct is just a wrapper around an existing type. This means that the Millimeters type has its own type identity, which is different from the type identity of the f64 type.
This type identity is important because it allows us to define functions that only accept values of the Millimeters type. For example, we could define a function that takes a value of the Millimeters type and returns a value of the Millimeters type, like this:
fn double(m: Millimeters) -> Millimeters {
Millimeters(m.0 \* 2.0)
}
This function takes a value of the Millimeters type and multiplies its value by 2. The result is a new value of the Millimeters type. We can call this function with a value of the Millimeters type like this:
let m2 = double(m);
This function would not be possible with a struct with one field. If we define a function that takes a Point struct as an argument, we can pass any struct that has a single f64 field as an argument. For example, we could define a function like this:
fn double_x(p: Point) -> Point {
Point { x: p.x \* 2.0 }
}
This function takes a Point struct and multiplies its x field by 2. The result is a new Point struct. However, we can also pass a struct with one field of a different type as an argument. For example, we could define a struct like this:
struct Centimeters(f64);
This struct is similar to the Millimeters struct, but it uses the name Centimeters instead. We can pass a value of the Centimeters struct to the double\_x function like this:
let c = Centimeters(3.0);
let p2 = double_x(c);
This is because the double\_x function takes a Point struct as an argument, and the Centimeters struct is not a Point struct. However, it has a single field of type f64, which is the same as the type of the x field in the Point struct. This means that the Centimeters struct can be implicitly converted to a Point struct, and the double\_x function can be called with the Centimeters struct as an argument.
This is not possible with the Millimeters type. If we try to pass a value of the Millimeters type to the double\_x function, we will get a type error. This is because the Millimeters type is a distinct type, and it cannot be implicitly converted to any other type.
This is one of the main advantages of the New Type idiom. It allows us to define a new type that has its own type identity, and it cannot be implicitly converted to any other type. This can help to prevent programming errors, and it can make our code more robust and maintainable.
Another advantage of the New Type idiom is that it can help to make our code more expressive. When we define a new type, we can give it a name that describes its purpose. For example, we could define a new type called Distance that is a wrapper around an f64 type. We could use this type to represent distances in meters, like this:
struct Distance(f64);
We can then define a function that takes a value of the Distance type and returns a value of the Distance type, like this:
fn add(d1: Distance, d2: Distance) -> Distance {
Distance(d1.0 + d2.0)
}
This function takes two values of the Distance type and adds them together. The result is a new value of the Distance type. We can call this function with values of the Distance type like this:
let d3 = add(Distance(3.0), Distance(4.0));
This function is more expressive than a function that takes two f64 values and returns an f64 value. When we use the Distance type, we are explicitly stating that the function takes two distances as arguments, and it returns a distance as a result. This can make our code easier to read and understand, and it can help to prevent programming errors.
In conclusion, the New Type idiom is a powerful feature of Rust's type system. It allows us to define a new type that is a wrapper around an existing type. This new type has its own type identity, and it cannot be implicitly converted to any other type. This can help to prevent programming errors, and it can make our code more robust and maintainable. The New Type idiom can also help to make our code more expressive, and it can make it easier to read and understand.
| Reference | Description |
|---|---|
| The Newtype Pattern | The official Rust documentation on the Newtype pattern. |
| Too Many Lists | A Rust tutorial that demonstrates the use of the Newtype pattern. |
| The Nomicon: Newtype Pattern | The Rustonomicon, a guide to advanced Rust concepts, on the Newtype pattern. |